From f43710567cec54dd91250eb9e40bb8be950fa18a Mon Sep 17 00:00:00 2001 From: Real Codesiman Date: Tue, 15 Sep 2026 09:02:13 +0700 Subject: [PATCH 1/2] feat(connections): unify channel/app integration connect lifecycle Replace ~22 per-provider connect/disconnect action pairs and three cookie-based OAuth picker flows with one connection-providers / connections / connect-sessions resource family, backed by a new server-side ConnectSession table instead of pending-auth cookies. The same registry and business services now serve the builder UI (private oRPC), the public API (workspace tokens), and MCP (generated from the shared OpenAPI document), so every IntegrationType is connectable and discoverable through one consistent surface. Legacy per-provider public lifecycle routes remain but are marked deprecated. --- .../public-spec-operations.test.ts.snap | 65 + ...nel-connect-credential-consistency.test.ts | 192 +- .../__tests__/channel-route-guards.test.ts | 8 - .../channels-create-messenger-handoff.test.ts | 6 - .../channels-create-messenger-reuse.test.ts | 164 +- .../channels-create-platform-owner.test.ts | 6 - .../channels-create-visibility-guard.test.ts | 6 - .../channels-instagram-routes.test.ts | 198 + .../__tests__/channels-public-scope.test.ts | 113 - apps/builder/__tests__/connect-client.test.ts | 25 +- apps/builder/__tests__/connect-routes.test.ts | 19 +- .../__tests__/connect-session-message.test.ts | 40 + .../__tests__/connections-public-api.test.ts | 664 + .../connections-public-scope.test.ts | 258 + .../connections-public-spec-flat.test.ts | 119 + ...facebook-pending-auth.cookie-store.test.ts | 243 - .../__tests__/facebook-pending-auth.test.ts | 216 - .../__tests__/instagram-accounts.test.tsx | 30 +- .../instagram-facebook-select-page.test.tsx | 115 +- .../instagram-select-account-action.test.ts | 235 +- ...ram-select-account-facebook-action.test.ts | 243 +- .../__tests__/instagram-select-page.test.tsx | 88 +- ...egration-actions-create-disconnect.test.ts | 125 + .../integration-llm-connect-actions.test.ts | 2 +- .../integration-webhook-freeze.test.ts | 23 + .../__tests__/integrations-public-api.test.ts | 103 +- .../integrations-public-scope.test.ts | 62 - ...saging-ads-oauth-referer-roundtrip.test.ts | 15 + .../__tests__/messenger-pages.test.tsx | 16 +- .../messenger-select-page-action.test.ts | 243 +- .../__tests__/messenger-select-page.test.tsx | 157 +- .../oauth-callback-connect-session.test.ts | 428 + .../oauth-reconnect-callback.test.ts | 105 +- .../__tests__/resolve-connect-session.test.ts | 87 +- .../select-facebook-accounts.test.tsx | 16 +- .../__tests__/smtp-actions-thin.test.ts | 1 + .../smtp-integrations-public-api.test.ts | 4 +- .../user-persistent-menus-public-api.test.ts | 4 +- .../__tests__/webchats-public-api.test.ts | 4 +- .../__tests__/webhooks-public-api.test.ts | 6 +- .../workspace-token-scope-registry.test.ts | 12 +- apps/builder/messages/ar.json | 17 +- apps/builder/messages/da.json | 17 +- apps/builder/messages/de.json | 17 +- apps/builder/messages/en.json | 17 +- apps/builder/messages/es.json | 17 +- apps/builder/messages/fi.json | 17 +- apps/builder/messages/fr.json | 17 +- apps/builder/messages/he.json | 17 +- apps/builder/messages/id.json | 17 +- apps/builder/messages/it.json | 17 +- apps/builder/messages/ja.json | 17 +- apps/builder/messages/nl.json | 17 +- apps/builder/messages/pt-BR.json | 17 +- apps/builder/messages/pt-PT.json | 17 +- apps/builder/messages/ro.json | 17 +- apps/builder/messages/sv.json | 17 +- apps/builder/messages/tr.json | 17 +- apps/builder/messages/vi.json | 17 +- apps/builder/messages/zh-CN.json | 17 +- apps/builder/messages/zh-TW.json | 17 +- apps/builder/package.json | 1 + .../channels/create/messenger/route.ts | 102 +- .../app/(no-sidebar)/channels/create/page.tsx | 14 +- .../channels/instagram-facebook/route.ts | 68 + .../instagram-facebook/select/page.tsx | 109 +- .../(no-sidebar)/channels/instagram/route.ts | 70 + .../channels/instagram/select/page.tsx | 48 +- .../channels/messenger/select/page.tsx | 107 +- .../app/connect/[sessionId]/auto-refresh.tsx | 29 + .../src/app/connect/[sessionId]/page.tsx | 71 + .../connect/[sessionId]/resolve-message.ts | 46 + .../integrations/[...integration]/callback.ts | 250 +- .../integrations/[...integration]/webhook.ts | 15 +- .../ads-campaign/actions/disconnect.action.ts | 4 +- .../src/features/ads-campaign/api/public.ts | 8 + .../features/channel-connect/lib/registry.ts | 15 +- .../lib/resolve-connect-session.ts | 51 +- .../actions/disconnect-connection.action.ts | 32 + .../src/features/connections/api/index.ts | 1 + .../src/features/connections/api/private.ts | 386 + .../src/features/connections/api/public.ts | 428 + .../lib/connect-session-resource.ts | 18 + .../lib/resolve-connect-credential.ts | 69 + .../connections/lib/resolve-provider.ts | 192 + .../features/connections/schema/request.ts | 53 + .../features/connections/schema/resource.ts | 114 + .../actions/disconnect.action.ts | 4 +- .../features/external-calendars/api/public.ts | 11 + .../features/external-webhooks/api/public.ts | 2 +- .../actions/disconnect.action.ts | 2 +- .../actions/disconnect.action.ts | 1 + .../features/integration-claude/lib/index.ts | 5 +- .../actions/disconnect.action.ts | 1 + .../integration-deepseek/lib/index.ts | 5 +- .../actions/disconnect.action.ts | 2 +- .../actions/disconnect.action.ts | 4 +- .../actions/connect.action.ts | 9 +- .../actions/disconnect.action.ts | 1 + .../actions/disconnect.action.ts | 2 +- .../actions/connect-account-facebook.ts | 261 +- .../actions/connect-account.ts | 262 +- .../integration-instagram/api/connect.ts | 2 + .../components/instagram-accounts.tsx | 30 +- .../components/select-accounts.tsx | 17 +- .../components/select-facebook-accounts.tsx | 4 +- .../schema/action-facebook.ts | 11 +- .../integration-instagram/schema/action.ts | 11 +- .../actions/disconnect.action.ts | 2 +- .../actions/disconnect.action.ts | 2 +- .../actions/disconnect.action.ts | 2 +- .../actions/connect-page.ts | 313 +- .../integration-messenger/api/connect.ts | 1 + .../integration-messenger/api/public.ts | 2 +- .../components/messenger-pages.tsx | 4 +- .../components/select-account.tsx | 8 +- .../actions/sync-message-templates.ts | 27 +- .../integration-messenger/schema/action.ts | 1 + .../actions/disconnect.action.ts | 2 +- .../actions/disconnect.action.ts | 31 +- .../actions/connect.action.ts | 9 +- .../actions/disconnect.action.ts | 1 + .../actions/disconnect.action.ts | 1 + .../integration-openrouter/lib/index.ts | 5 +- .../actions/disconnect.action.ts | 2 +- .../actions/delete-smtp.action.ts | 4 +- .../features/integration-smtp/api/public.ts | 2 +- .../actions/delete-webchat.action.ts | 4 +- .../integration-webchat/api/public.ts | 2 +- .../integration-webchat/lib/widget-css.tsx | 2 +- .../features/integration-zalo/api/public.ts | 2 +- .../features/integrations/api/public/ai.ts | 62 +- .../features/integrations/api/public/crud.ts | 5 +- .../src/features/personas/api/public.ts | 2 +- .../user-persistent-menus/api/public.ts | 2 +- .../src/features/webhooks/api/public.ts | 2 +- .../workspaces/lib/workspace-token-scopes.ts | 11 +- apps/builder/src/integration.ts | 126 +- apps/builder/src/lib/facebook-pending-auth.ts | 138 - apps/builder/src/lib/integration-actions.ts | 38 +- apps/builder/src/lib/oauth-referer.ts | 23 + .../builder/src/lib/orpc/orpc-error-helper.ts | 115 + apps/builder/src/routers/index.ts | 10 + apps/builder/src/routers/public.ts | 8 + .../purge-expired-connect-sessions.test.ts | 42 + apps/worker/package.json | 1 + .../purge-expired-connect-sessions.ts | 20 + .../schedule/handlers/register-schedules.ts | 14 + apps/worker/src/schedule/worker.ts | 5 + apps/worker/src/services/integrations.ts | 52 +- docs/developer/workspace-api-tokens.md | 40 +- .../active-campaign/src/integration.ts | 45 + integrations/api/src/integration.ts | 14 + integrations/chatbotx/src/integration.ts | 14 + integrations/drip/src/integration.ts | 42 + .../__tests__/connection-oauth.test.ts | 91 + integrations/facebook-ads/src/integration.ts | 62 +- integrations/get-response/src/integration.ts | 41 + .../__tests__/connection-oauth.test.ts | 139 + .../google-calendar/src/integration.ts | 72 +- .../__tests__/connection-oauth.test.ts | 99 + integrations/google-sheets/src/integration.ts | 56 + .../__tests__/connection-oauth.test.ts | 187 + .../instagram-facebook/src/integration.ts | 122 + integrations/instagram/src/integration.ts | 97 + integrations/klaviyo/src/integration.ts | 48 + integrations/mailchimp/src/integration.ts | 46 + integrations/mailer-lite/src/integration.ts | 48 + .../__tests__/connection-oauth.test.ts | 163 + integrations/messenger/src/integration.ts | 128 + integrations/moosend/src/integration.ts | 46 + integrations/sendgrid/src/integration.ts | 46 + integrations/smtp/src/integration.ts | 57 + integrations/telegram/src/integration.ts | 26 +- .../tiktok/__tests__/connection-oauth.test.ts | 113 + integrations/tiktok/src/integration.ts | 92 +- integrations/webchat/src/integration.ts | 15 + integrations/whatsapp/src/integration.ts | 38 +- .../zalo/__tests__/connection-oauth.test.ts | 99 + integrations/zalo/src/integration.ts | 72 +- .../appointment-service-webview.test.ts | 3 +- .../__tests__/integration-service.test.ts | 4 + packages/business/package.json | 4 + .../connect-session/__tests__/service.test.ts | 358 + .../business/src/connect-session/index.ts | 1 + .../business/src/connect-session/service.ts | 374 + .../__tests__/state-service.test.ts | 255 + .../src/connection/__tests__/state.test.ts | 189 + packages/business/src/connection/adapter.ts | 57 + .../src/connection/credential-providers.ts | 154 + packages/business/src/connection/index.ts | 5 + .../business/src/connection/state-service.ts | 316 + packages/business/src/connection/state.ts | 162 + .../business/src/connection/store-bindings.ts | 547 + packages/business/src/errors.ts | 87 + packages/business/src/inbox/service.ts | 44 +- packages/business/src/index.ts | 2 + .../__tests__/verify.test.ts | 19 +- .../src/integration-ai-provider/verify.ts | 32 +- .../__tests__/auth-store.test.ts | 229 + .../src/integration-context/auth-store.ts | 88 +- .../find-token-refresh-errors.test.ts | 93 + packages/business/src/integration/service.ts | 153 +- .../connections/__tests__/registry.test.ts | 79 + .../connections/__tests__/service.test.ts | 1417 + packages/connections/package.json | 51 + packages/connections/src/index.ts | 2 + packages/connections/src/logger.ts | 3 + packages/connections/src/registry.ts | 142 + packages/connections/src/service.ts | 1224 + packages/connections/tsconfig.json | 7 + packages/connections/vitest.config.ts | 1 + .../migration.sql | 18 + .../snapshot.json | 44204 +++++++++++++++ .../migration.sql | 65 + .../snapshot.json | 45114 ++++++++++++++++ packages/database/package.json | 1 + .../database/scripts/backfill-connections.ts | 438 + .../database/src/partials/connect-session.ts | 66 + packages/database/src/partials/connection.ts | 23 + packages/database/src/partials/index.ts | 2 + .../src/partials/workspace-api-token.ts | 3 +- .../database/src/relations/connect-session.ts | 28 + packages/database/src/relations/connection.ts | 28 + packages/database/src/relations/inbox.ts | 5 + packages/database/src/relations/index.ts | 4 + .../database/src/relations/integration.ts | 4 + .../src/repositories/connect-session/index.ts | 1 + .../connect-session/repository.ts | 129 + .../src/repositories/connection/index.ts | 1 + .../src/repositories/connection/repository.ts | 152 + packages/database/src/repositories/index.ts | 2 + .../database/src/schema/connect-session.ts | 112 + packages/database/src/schema/connection.ts | 101 + packages/database/src/schema/index.ts | 2 + packages/database/src/types.ts | 2 + .../sdk/__tests__/ensure-fresh-auth.test.ts | 135 + packages/sdk/src/index.ts | 1 + packages/sdk/src/lib/connection.ts | 126 + packages/sdk/src/lib/integration.ts | 42 +- packages/sdk/src/lib/shared/context.ts | 7 + packages/utils/package.json | 1 + packages/utils/src/connection.ts | 117 + .../src/queues/schedule/index.ts | 7 + pnpm-lock.yaml | 112 + 245 files changed, 105152 insertions(+), 3020 deletions(-) create mode 100644 apps/builder/__tests__/channels-instagram-routes.test.ts delete mode 100644 apps/builder/__tests__/channels-public-scope.test.ts create mode 100644 apps/builder/__tests__/connect-session-message.test.ts create mode 100644 apps/builder/__tests__/connections-public-api.test.ts create mode 100644 apps/builder/__tests__/connections-public-scope.test.ts create mode 100644 apps/builder/__tests__/connections-public-spec-flat.test.ts delete mode 100644 apps/builder/__tests__/facebook-pending-auth.cookie-store.test.ts delete mode 100644 apps/builder/__tests__/facebook-pending-auth.test.ts create mode 100644 apps/builder/__tests__/integration-actions-create-disconnect.test.ts delete mode 100644 apps/builder/__tests__/integrations-public-scope.test.ts create mode 100644 apps/builder/__tests__/oauth-callback-connect-session.test.ts create mode 100644 apps/builder/src/app/(no-sidebar)/channels/instagram-facebook/route.ts create mode 100644 apps/builder/src/app/(no-sidebar)/channels/instagram/route.ts create mode 100644 apps/builder/src/app/connect/[sessionId]/auto-refresh.tsx create mode 100644 apps/builder/src/app/connect/[sessionId]/page.tsx create mode 100644 apps/builder/src/app/connect/[sessionId]/resolve-message.ts create mode 100644 apps/builder/src/features/connections/actions/disconnect-connection.action.ts create mode 100644 apps/builder/src/features/connections/api/index.ts create mode 100644 apps/builder/src/features/connections/api/private.ts create mode 100644 apps/builder/src/features/connections/api/public.ts create mode 100644 apps/builder/src/features/connections/lib/connect-session-resource.ts create mode 100644 apps/builder/src/features/connections/lib/resolve-connect-credential.ts create mode 100644 apps/builder/src/features/connections/lib/resolve-provider.ts create mode 100644 apps/builder/src/features/connections/schema/request.ts create mode 100644 apps/builder/src/features/connections/schema/resource.ts delete mode 100644 apps/builder/src/lib/facebook-pending-auth.ts create mode 100644 apps/worker/__tests__/purge-expired-connect-sessions.test.ts create mode 100644 apps/worker/src/schedule/handlers/purge-expired-connect-sessions.ts create mode 100644 integrations/facebook-ads/__tests__/connection-oauth.test.ts create mode 100644 integrations/google-calendar/__tests__/connection-oauth.test.ts create mode 100644 integrations/google-sheets/__tests__/connection-oauth.test.ts create mode 100644 integrations/instagram-facebook/__tests__/connection-oauth.test.ts create mode 100644 integrations/messenger/__tests__/connection-oauth.test.ts create mode 100644 integrations/tiktok/__tests__/connection-oauth.test.ts create mode 100644 integrations/zalo/__tests__/connection-oauth.test.ts create mode 100644 packages/business/src/connect-session/__tests__/service.test.ts create mode 100644 packages/business/src/connect-session/index.ts create mode 100644 packages/business/src/connect-session/service.ts create mode 100644 packages/business/src/connection/__tests__/state-service.test.ts create mode 100644 packages/business/src/connection/__tests__/state.test.ts create mode 100644 packages/business/src/connection/adapter.ts create mode 100644 packages/business/src/connection/credential-providers.ts create mode 100644 packages/business/src/connection/index.ts create mode 100644 packages/business/src/connection/state-service.ts create mode 100644 packages/business/src/connection/state.ts create mode 100644 packages/business/src/connection/store-bindings.ts rename apps/builder/__tests__/integration-ai-verify-api-key.test.ts => packages/business/src/integration-ai-provider/__tests__/verify.test.ts (72%) rename apps/builder/src/features/integration-ai/lib/verify-api-key.ts => packages/business/src/integration-ai-provider/verify.ts (75%) create mode 100644 packages/business/src/integration-context/__tests__/auth-store.test.ts create mode 100644 packages/business/src/integration/__tests__/find-token-refresh-errors.test.ts create mode 100644 packages/connections/__tests__/registry.test.ts create mode 100644 packages/connections/__tests__/service.test.ts create mode 100644 packages/connections/package.json create mode 100644 packages/connections/src/index.ts create mode 100644 packages/connections/src/logger.ts create mode 100644 packages/connections/src/registry.ts create mode 100644 packages/connections/src/service.ts create mode 100644 packages/connections/tsconfig.json create mode 100644 packages/connections/vitest.config.ts create mode 100644 packages/database/drizzle/20260914163422_consolidate_connection_scope/migration.sql create mode 100644 packages/database/drizzle/20260914163422_consolidate_connection_scope/snapshot.json create mode 100644 packages/database/drizzle/20260914170910_add_connection_tables/migration.sql create mode 100644 packages/database/drizzle/20260914170910_add_connection_tables/snapshot.json create mode 100644 packages/database/scripts/backfill-connections.ts create mode 100644 packages/database/src/partials/connect-session.ts create mode 100644 packages/database/src/partials/connection.ts create mode 100644 packages/database/src/relations/connect-session.ts create mode 100644 packages/database/src/relations/connection.ts create mode 100644 packages/database/src/repositories/connect-session/index.ts create mode 100644 packages/database/src/repositories/connect-session/repository.ts create mode 100644 packages/database/src/repositories/connection/index.ts create mode 100644 packages/database/src/repositories/connection/repository.ts create mode 100644 packages/database/src/schema/connect-session.ts create mode 100644 packages/database/src/schema/connection.ts create mode 100644 packages/sdk/__tests__/ensure-fresh-auth.test.ts create mode 100644 packages/sdk/src/lib/connection.ts create mode 100644 packages/utils/src/connection.ts diff --git a/apps/builder/__tests__/__snapshots__/public-spec-operations.test.ts.snap b/apps/builder/__tests__/__snapshots__/public-spec-operations.test.ts.snap index 38ddcdf85f..b0cefcadf5 100644 --- a/apps/builder/__tests__/__snapshots__/public-spec-operations.test.ts.snap +++ b/apps/builder/__tests__/__snapshots__/public-spec-operations.test.ts.snap @@ -612,6 +612,71 @@ exports[`public API spec — operation naming guard > operation list (operationI "operationId": "channels.typing", "path": "/v1/channels/api/typing", }, + { + "method": "GET", + "operationId": "connectionProviders.list", + "path": "/v1/connection-providers", + }, + { + "method": "POST", + "operationId": "connections.create", + "path": "/v1/connections", + }, + { + "method": "DELETE", + "operationId": "connections.disconnect", + "path": "/v1/connections/{id}", + }, + { + "method": "GET", + "operationId": "connections.get", + "path": "/v1/connections/{id}", + }, + { + "method": "GET", + "operationId": "connections.list", + "path": "/v1/connections", + }, + { + "method": "POST", + "operationId": "connections.reconnect", + "path": "/v1/connections/{id}/reconnect", + }, + { + "method": "POST", + "operationId": "connections.refresh", + "path": "/v1/connections/{id}/refresh", + }, + { + "method": "PATCH", + "operationId": "connections.update", + "path": "/v1/connections/{id}", + }, + { + "method": "POST", + "operationId": "connections.verify", + "path": "/v1/connections/{id}/verify", + }, + { + "method": "DELETE", + "operationId": "connectSessions.cancel", + "path": "/v1/connect-sessions/{id}", + }, + { + "method": "POST", + "operationId": "connectSessions.connectTargets", + "path": "/v1/connect-sessions/{id}/targets", + }, + { + "method": "GET", + "operationId": "connectSessions.get", + "path": "/v1/connect-sessions/{id}", + }, + { + "method": "POST", + "operationId": "connectSessions.submitInput", + "path": "/v1/connect-sessions/{id}/input", + }, { "method": "POST", "operationId": "contacts.addTags", diff --git a/apps/builder/__tests__/channel-connect-credential-consistency.test.ts b/apps/builder/__tests__/channel-connect-credential-consistency.test.ts index c18498e340..4fc99e2048 100644 --- a/apps/builder/__tests__/channel-connect-credential-consistency.test.ts +++ b/apps/builder/__tests__/channel-connect-credential-consistency.test.ts @@ -3,62 +3,32 @@ import { beforeEach, describe, expect, test, vi } from "vitest" // --------------------------------------------------------------------------- -// The OAuth completion legs (the three "select" actions plus the messenger -// reuse-check route) must resolve the platform credential owner from the -// SAME workspaceId the start leg (`/channels/create`) used — never from the -// request host. Those legs run post-relay on the broker or branded host -// interchangeably, so a host-derived completion leg could silently pick a -// different OAuth app than the one the start leg authorized against, -// breaking the token exchange. -// -// Messenger's `connectMessengerPage` (plan §2.4/§4.7) gets its `workspaceId` -// from the encrypted, httpOnly pending-auth cookie — never client input — -// so this test pins that it forwards the COOKIE's workspaceId into -// `resolvePlatformOwnerId`, and that a schema-invalid/missing cookie is -// rejected with a `sessionError` before the resolver is ever called. -// Instagram's two legs (phase 4) now go through the same -// `resolveConnectSession` helper, so they get the identical treatment: the -// wire payload carries only `igId`, never `workspaceId`. +// The OAuth completion legs (the three "select" actions) must resolve the +// platform credential from the SAME owner the start leg (`/channels/create` +// or `channels/create/messenger/route.ts`) resolved — never re-derive it +// from the request host. Under the unified `ConnectSession` model this is a +// structural invariant rather than a call-site convention: the session row +// stores `platformOwnerId` once, at creation time, and every completion leg +// (`resolveConnectSession`) reads it straight off that row — there is no +// `resolvePlatformOwnerId`/host-derived re-resolution step in the +// completion leg to drift. This test pins that `platformCredentialService +// .resolveForOwner` is called with the SESSION's stored owner, and that a +// missing/expired session short-circuits before any credential lookup. // --------------------------------------------------------------------------- -const { - mockResolvePlatformOwnerId, - mockResolveForOwner, - mockReadPendingAuth, - mockWorkspaceFind, - mockIsMember, -} = vi.hoisted(() => ({ - mockResolvePlatformOwnerId: vi.fn(async () => "resolved-owner-1"), - mockResolveForOwner: vi.fn(async () => undefined), - mockReadPendingAuth: vi.fn( - async (): Promise<{ - userToken: string - workspaceId: string - referer: string - version: string - expiresAt: number - } | null> => ({ - userToken: "user-token-1", - workspaceId: "ws-1", - referer: "/channels/create", - version: "v23.0", - expiresAt: Date.now() + 600_000, - }), - ), - mockWorkspaceFind: vi.fn(async () => ({ - id: "ws-1", - ownerId: "owner-1", - })), - mockIsMember: vi.fn(async () => true), -})) - -// A passthrough action-client chain: `.inputSchema()`/`.action()` just -// return their handler so the test can call it directly with a hand-built -// `{ ctx, parsedInput }`, without instantiating the real safe-action / -// next-safe-action machinery. Mirrors the pattern in -// `instagram-facebook-settings-actions.test.ts`. -vi.mock("@/lib/platform-credential-owner", () => ({ - resolvePlatformOwnerId: mockResolvePlatformOwnerId, +const { mockFindById, mockResolveForOwner, mockWorkspaceFind, mockIsMember } = + vi.hoisted(() => ({ + mockFindById: vi.fn(), + mockResolveForOwner: vi.fn(async () => undefined), + mockWorkspaceFind: vi.fn(async () => ({ + id: "ws-1", + ownerId: "owner-1", + })), + mockIsMember: vi.fn(async () => true), + })) + +vi.mock("@chatbotx.io/business/connect-session", () => ({ + connectSessionService: { findById: mockFindById }, })) // Bypassed entirely — this test is about credential-owner resolution, not @@ -84,11 +54,13 @@ vi.mock("@chatbotx.io/business", () => ({ findConnectedPageIds: vi.fn(async () => new Set()), connectPage: vi.fn(), updateUserInfo: vi.fn(), + findByInboxId: vi.fn(), }, instagramIntegrationService: { findConnectedIgIds: vi.fn(async () => new Set()), connectAccount: vi.fn(), updateUserInfo: vi.fn(), + findByInboxId: vi.fn(), }, tagSyncService: { enqueueChannelScan: vi.fn() }, userQuotaService: { getAccessState: vi.fn(async () => ({ blocked: false })) }, @@ -96,6 +68,14 @@ vi.mock("@chatbotx.io/business", () => ({ buildContext: vi.fn(async () => ({})), })) +// The real connect actions never reach `connectionService.connectTargets` in +// this test — `mockResolveForOwner` defaults to `undefined`, so +// `resolveConnectSession` throws `credentialMissingException` first — this +// stub only satisfies the module import. +vi.mock("@chatbotx.io/connections", () => ({ + connectionService: { connectTargets: vi.fn() }, +})) + // The REAL session/item-outcome mapping table — `resolveConnectSession` // (called by `connectMessengerPage`) throws genuine exceptions from the // (also real, below) `@chatbotx.io/business/errors`, so this file lets the @@ -129,45 +109,14 @@ vi.mock("@chatbotx.io/database/client", () => ({ isDatabaseError: vi.fn(() => false), })) -vi.mock("@chatbotx.io/database/schema", async (importOriginal) => { - const actual = - await importOriginal() - return { - ...actual, - integrationInstagramModel: {}, - integrationMessengerModel: {}, - } -}) - vi.mock("@chatbotx.io/integration-messenger", () => ({ integration: { runChannelHandler: vi.fn() }, - getUserPages: vi.fn(async () => ({ - pages: [ - { - id: "p1", - name: "Page", - access_token: "page-token", - isConnectable: true, - }, - ], - bmLookupFailed: false, - })), -})) -vi.mock("@chatbotx.io/integration-messenger/apis/page", () => ({ - exchangeLongLivedToken: vi.fn(), - subscribePageToAppWebhook: vi.fn(), })) vi.mock("@chatbotx.io/integration-instagram", () => ({ integration: { runChannelHandler: vi.fn() }, - subscribePageToInstagramWebhook: vi.fn(), })) vi.mock("@chatbotx.io/integration-instagram-facebook", () => ({ integration: { runChannelHandler: vi.fn() }, - subscribePageToInstagramWebhook: vi.fn(), -})) -vi.mock("@chatbotx.io/sdk", () => ({ - AuthType: { oauth2: "oauth2" }, - SdkException: class SdkException extends Error {}, })) vi.mock("@chatbotx.io/utils", async (importOriginal) => { const actual = await importOriginal() @@ -188,13 +137,6 @@ vi.mock("@/features/integration-webchat/lib", () => ({ vi.mock("@/features/workspaces/actions/upload-logo", () => ({ updateWorkspaceLogo: vi.fn(), })) -vi.mock("@/lib/facebook-pending-auth", () => ({ - FB_MESSENGER_PENDING_AUTH_COOKIE: "fb_messenger_pending_auth", - FB_INSTAGRAM_FACEBOOK_PENDING_AUTH_COOKIE: - "fb_instagram_facebook_pending_auth", - FB_INSTAGRAM_PENDING_AUTH_COOKIE: "fb_instagram_pending_auth", - readPendingAuth: mockReadPendingAuth, -})) vi.mock("@/lib/integration-user-info", () => ({ persistIntegrationUserInfo: vi.fn(), })) @@ -212,69 +154,77 @@ const { connectInstagramAccountViaFacebook } = await import( "../src/features/integration-instagram/actions/connect-account-facebook" ) +const session = { + id: "session-1", + workspaceId: "ws-1", + platformOwnerId: "owner-1", + provider: "messenger", + status: "awaiting_selection", + targets: [{ id: "p1", name: "Page", selectable: true }], +} + describe("channel connect completion legs never re-derive the credential owner from the host", () => { beforeEach(() => { vi.clearAllMocks() - mockResolvePlatformOwnerId.mockResolvedValue("resolved-owner-1") // Credential missing short-circuits each action right after the // resolver call — exactly the point this test needs to observe, without // running the rest of the (heavily mocked) connect transaction. mockResolveForOwner.mockResolvedValue(undefined) - mockReadPendingAuth.mockResolvedValue({ - userToken: "user-token-1", - workspaceId: "ws-1", - referer: "/channels/create", - version: "v23.0", - expiresAt: Date.now() + 600_000, - }) + mockFindById.mockResolvedValue(session) mockWorkspaceFind.mockResolvedValue({ id: "ws-1", ownerId: "owner-1" }) mockIsMember.mockResolvedValue(true) }) - test("connectMessengerPage resolves the credential owner from the pending-auth cookie's workspaceId", async () => { - await connectMessengerPage({ userId: "user-1", pageId: "p1" }).catch( - () => undefined, - ) - - expect(mockResolvePlatformOwnerId).toHaveBeenCalledWith({ + test("connectMessengerPage resolves the credential from the session's stored platformOwnerId", async () => { + await connectMessengerPage({ userId: "user-1", - workspaceId: "ws-1", + sessionId: "session-1", + pageId: "p1", + }).catch(() => undefined) + + expect(mockResolveForOwner).toHaveBeenCalledWith({ + ownerId: "owner-1", + type: "messenger", }) }) - test("connectInstagramAccount resolves the credential owner from the pending-auth cookie's workspaceId", async () => { - await connectInstagramAccount({ userId: "user-1", igId: "ig1" }).catch( - () => undefined, - ) - - expect(mockResolvePlatformOwnerId).toHaveBeenCalledWith({ + test("connectInstagramAccount resolves the credential from the session's stored platformOwnerId", async () => { + await connectInstagramAccount({ userId: "user-1", - workspaceId: "ws-1", + sessionId: "session-1", + igId: "ig1", + }).catch(() => undefined) + + expect(mockResolveForOwner).toHaveBeenCalledWith({ + ownerId: "owner-1", + type: "instagram", }) }) - test("connectInstagramAccountViaFacebook resolves the credential owner from the pending-auth cookie's workspaceId", async () => { + test("connectInstagramAccountViaFacebook resolves the credential from the session's stored platformOwnerId", async () => { await connectInstagramAccountViaFacebook({ userId: "user-1", + sessionId: "session-1", igId: "ig1", }).catch(() => undefined) - expect(mockResolvePlatformOwnerId).toHaveBeenCalledWith({ - userId: "user-1", - workspaceId: "ws-1", + expect(mockResolveForOwner).toHaveBeenCalledWith({ + ownerId: "owner-1", + type: "instagramFacebook", }) }) - test("connectMessengerPage never calls the resolver when the pending-auth cookie is missing/schema-invalid", async () => { - mockReadPendingAuth.mockResolvedValue(null) + test("connectMessengerPage never calls the credential resolver when the session is missing/expired", async () => { + mockFindById.mockResolvedValue(null) const result = await connectMessengerPage({ userId: "user-1", + sessionId: "session-1", pageId: "p1", }) expect(result).toEqual({ kind: "sessionError", code: "sessionExpired" }) - expect(mockResolvePlatformOwnerId).not.toHaveBeenCalled() + expect(mockResolveForOwner).not.toHaveBeenCalled() expect(mockWorkspaceFind).not.toHaveBeenCalled() }) }) diff --git a/apps/builder/__tests__/channel-route-guards.test.ts b/apps/builder/__tests__/channel-route-guards.test.ts index 3265e927bb..d38529fa57 100644 --- a/apps/builder/__tests__/channel-route-guards.test.ts +++ b/apps/builder/__tests__/channel-route-guards.test.ts @@ -111,14 +111,6 @@ vi.mock( }), ) -vi.mock("@/features/integration-instagram/libs/oauth", () => ({ - generateInstagramRedirectUri: vi.fn(async () => ""), -})) - -vi.mock("@/features/integration-instagram/libs/oauth-facebook", () => ({ - generateInstagramFacebookRedirectUri: vi.fn(async () => ""), -})) - vi.mock("@/features/integration-messenger/libs/oauth", () => ({ generateMessengerRedirectUri: vi.fn(async () => ""), })) diff --git a/apps/builder/__tests__/channels-create-messenger-handoff.test.ts b/apps/builder/__tests__/channels-create-messenger-handoff.test.ts index ecc5d5fb5d..422eaf4564 100644 --- a/apps/builder/__tests__/channels-create-messenger-handoff.test.ts +++ b/apps/builder/__tests__/channels-create-messenger-handoff.test.ts @@ -63,12 +63,6 @@ vi.mock( "@/features/integration-instagram/components/instagram-login-select", () => ({ InstagramLoginSelect: () => null }), ) -vi.mock("@/features/integration-instagram/libs/oauth", () => ({ - generateInstagramRedirectUri: vi.fn(async () => ""), -})) -vi.mock("@/features/integration-instagram/libs/oauth-facebook", () => ({ - generateInstagramFacebookRedirectUri: vi.fn(async () => ""), -})) vi.mock("@/features/integration-telegram/components/telegram-connect", () => ({ TelegramConnect: () => null, })) diff --git a/apps/builder/__tests__/channels-create-messenger-reuse.test.ts b/apps/builder/__tests__/channels-create-messenger-reuse.test.ts index 4dbec1d2eb..b74164258f 100644 --- a/apps/builder/__tests__/channels-create-messenger-reuse.test.ts +++ b/apps/builder/__tests__/channels-create-messenger-reuse.test.ts @@ -3,46 +3,38 @@ import { beforeEach, describe, expect, test, vi } from "vitest" const { - mockBuildMessengerReferer, - mockCookieSet, - mockCookies, - mockEncryptAuth, - mockFindWorkspace, + mockConnectSessionCreate, mockFindWorkspaceById, - mockGenerateMessengerRedirectUri, mockGetCurrentUserId, + mockListAndAttachCandidates, mockRedirect, mockRequireWorkspacePermission, mockResolveForOwner, + mockResolveOAuthCredential, + mockStartSession, mockTryReuseFacebookSsoToken, + mockUpdateReturnUrl, mockWorkspaceCreate, } = vi.hoisted(() => ({ - mockBuildMessengerReferer: vi.fn(async () => "https://tenant.example.com"), - mockCookieSet: vi.fn(), - mockCookies: vi.fn(), - mockEncryptAuth: vi.fn(async () => "encrypted-token"), - mockFindWorkspace: vi.fn(async () => ({ ownerId: "owner-1" })), + mockConnectSessionCreate: vi.fn(), mockFindWorkspaceById: vi.fn(async () => ({ id: "ws-1", ownerId: "owner-1", })), - mockGenerateMessengerRedirectUri: vi.fn( - async () => "https://facebook.com/oauth-dialog", - ), mockGetCurrentUserId: vi.fn(async () => "user-1"), + mockListAndAttachCandidates: vi.fn(), mockRedirect: vi.fn((path: string) => { throw new Error(`redirect:${path}`) }), mockRequireWorkspacePermission: vi.fn(async () => undefined), mockResolveForOwner: vi.fn(), + mockResolveOAuthCredential: vi.fn(), + mockStartSession: vi.fn(), mockTryReuseFacebookSsoToken: vi.fn(), + mockUpdateReturnUrl: vi.fn(), mockWorkspaceCreate: vi.fn(async () => ({ id: "ws-new", ownerId: "user-1" })), })) -vi.mock("next/headers", () => ({ - cookies: mockCookies, -})) - vi.mock("next/navigation", () => ({ notFound: vi.fn(() => { throw new Error("not found") @@ -53,12 +45,29 @@ vi.mock("next/navigation", () => ({ vi.mock("@chatbotx.io/business", () => ({ platformCredentialService: { resolveForOwner: mockResolveForOwner }, workspaceService: { - find: mockFindWorkspace, findById: mockFindWorkspaceById, create: mockWorkspaceCreate, }, })) +vi.mock("@chatbotx.io/business/connect-session", () => ({ + connectSessionService: { + create: mockConnectSessionCreate, + updateReturnUrl: mockUpdateReturnUrl, + }, +})) + +vi.mock("@chatbotx.io/connections", () => ({ + connectionService: { + listAndAttachCandidates: mockListAndAttachCandidates, + startSession: mockStartSession, + }, +})) + +vi.mock("@/features/connections/lib/resolve-connect-credential", () => ({ + resolveOAuthCredential: mockResolveOAuthCredential, +})) + vi.mock("@/lib/platform-credential-owner", () => ({ resolvePlatformOwnerId: vi.fn(async () => "owner-1"), })) @@ -71,27 +80,6 @@ vi.mock("@/lib/auth/utils", () => ({ getCurrentUserId: mockGetCurrentUserId, })) -vi.mock("@/lib/facebook-pending-auth", async (importOriginal) => { - // The real `pendingAuthCookieOptions` — these tests assert the set site - // passes the helper's own value through, not a copy that could drift from - // it (the cookie's `path` is what makes the connect routes reachable). - const actual = - await importOriginal() - - return { - encryptAuth: mockEncryptAuth, - FB_MESSENGER_PENDING_AUTH_COOKIE: "fb_messenger_pending_auth", - FB_PENDING_AUTH_MAX_AGE: 600, - // The real writer: these tests assert what actually reaches the cookie - // store, including the expiry of the picker-scoped predecessor. - writePendingAuth: actual.writePendingAuth, - } -}) -vi.mock("@/features/integration-messenger/libs/oauth", () => ({ - buildMessengerReferer: mockBuildMessengerReferer, - generateMessengerRedirectUri: mockGenerateMessengerRedirectUri, -})) - vi.mock("@/features/integration-messenger/libs/sso-reuse", () => ({ tryReuseFacebookSsoToken: mockTryReuseFacebookSsoToken, })) @@ -113,32 +101,38 @@ function requestWithWorkspaceId(workspaceId: string | null) { return { nextUrl: url } as unknown as Parameters[0] } -/** Only the messenger credential resolves; every other channel is unconfigured. */ function resolveOnlyMessenger() { - mockResolveForOwner.mockImplementation(({ type }: { type: string }) => - Promise.resolve(type === "messenger" ? messengerCredential : undefined), - ) + mockResolveForOwner.mockResolvedValue(messengerCredential) } describe("GET /channels/create/messenger — Facebook SSO token reuse", () => { beforeEach(() => { vi.clearAllMocks() mockGetCurrentUserId.mockResolvedValue("user-1") - mockCookies.mockResolvedValue({ set: mockCookieSet }) + mockConnectSessionCreate.mockResolvedValue({ session: { id: "session-1" } }) + mockResolveOAuthCredential.mockResolvedValue({ + credential: { clientId: "app-id", clientSecret: "app-secret" }, + callbackUrl: "https://app.example.com/integrations/messenger/callback", + }) + mockStartSession.mockResolvedValue({ + session: { id: "session-2" }, + nextAction: { + type: "open_url", + url: "https://facebook.com/oauth-dialog", + }, + }) + mockUpdateReturnUrl.mockResolvedValue({ id: "session-2" }) resolveOnlyMessenger() }) - test("reuses a valid SSO token: writes the pending-auth cookie and redirects straight to the Page picker", async () => { + test("reuses a valid SSO token: attaches candidates and redirects to the Page picker session", async () => { mockTryReuseFacebookSsoToken.mockResolvedValue({ reusable: true, userToken: "long-lived-user-token", - userId: "fb-1", - userName: "Jane Doe", - userAvatarUrl: "https://example.com/a.png", }) await expect(GET(requestWithWorkspaceId("ws-1"))).rejects.toThrow( - "redirect:/channels/messenger/select", + "redirect:/channels/messenger/select?session=session-1", ) expect(mockRequireWorkspacePermission).toHaveBeenCalledWith( @@ -147,24 +141,25 @@ describe("GET /channels/create/messenger — Facebook SSO token reuse", () => { ) expect(mockFindWorkspaceById).toHaveBeenCalledWith({ id: "ws-1" }) expect(mockWorkspaceCreate).not.toHaveBeenCalled() - expect(mockEncryptAuth).toHaveBeenCalledWith( - expect.objectContaining({ - userToken: "long-lived-user-token", - userId: "fb-1", - userName: "Jane Doe", - userAvatarUrl: "https://example.com/a.png", - workspaceId: "ws-1", + expect(mockConnectSessionCreate).toHaveBeenCalledWith({ + workspaceId: "ws-1", + provider: "messenger", + purpose: "connect", + actorUserId: "user-1", + platformOwnerId: "owner-1", + }) + expect(mockListAndAttachCandidates).toHaveBeenCalledWith( + { id: "session-1" }, + { + authType: "oauth2", + clientId: "app-id", + clientSecret: "app-secret", + redirectUrl: "", version: "v23.0", - }), - ) - expect(mockCookieSet).toHaveBeenCalledWith( - "fb_messenger_pending_auth", - "encrypted-token", - // The connect route reads this cookie, so it must not be scoped to the - // picker page (see `pendingAuthCookieOptions`). - expect.objectContaining({ path: "/", httpOnly: true, sameSite: "lax" }), + tokens: { accessToken: "long-lived-user-token" }, + }, ) - expect(mockGenerateMessengerRedirectUri).not.toHaveBeenCalled() + expect(mockStartSession).not.toHaveBeenCalled() }) test("creates a workspace first when reusing a token with no workspaceId yet (first channel ever)", async () => { @@ -174,7 +169,7 @@ describe("GET /channels/create/messenger — Facebook SSO token reuse", () => { }) await expect(GET(requestWithWorkspaceId(null))).rejects.toThrow( - "redirect:/channels/messenger/select", + "redirect:/channels/messenger/select?session=session-1", ) expect(mockRequireWorkspacePermission).not.toHaveBeenCalled() @@ -182,7 +177,7 @@ describe("GET /channels/create/messenger — Facebook SSO token reuse", () => { data: { name: "New Workspace", ownerId: "user-1" }, createdBy: "user-1", }) - expect(mockEncryptAuth).toHaveBeenCalledWith( + expect(mockConnectSessionCreate).toHaveBeenCalledWith( expect.objectContaining({ workspaceId: "ws-new" }), ) }) @@ -194,9 +189,6 @@ describe("GET /channels/create/messenger — Facebook SSO token reuse", () => { mockTryReuseFacebookSsoToken.mockResolvedValue({ reusable: true, userToken: "long-lived-user-token", - userId: "fb-1", - userName: "Jane Doe", - userAvatarUrl: "https://example.com/a.png", }) mockWorkspaceCreate.mockRejectedValueOnce(workspaceLimitReachedException()) @@ -204,22 +196,36 @@ describe("GET /channels/create/messenger — Facebook SSO token reuse", () => { "redirect:/channels/create?error=workspaceLimitReached", ) - expect(mockCookieSet).not.toHaveBeenCalled() + expect(mockConnectSessionCreate).not.toHaveBeenCalled() + expect(mockListAndAttachCandidates).not.toHaveBeenCalled() }) - test("falls back to the full OAuth redirect when there is no reusable token", async () => { + test("starts a full OAuth session when there is no reusable token", async () => { mockTryReuseFacebookSsoToken.mockResolvedValue({ reusable: false }) await expect(GET(requestWithWorkspaceId("ws-1"))).rejects.toThrow( "redirect:https://facebook.com/oauth-dialog", ) - expect(mockEncryptAuth).not.toHaveBeenCalled() - expect(mockCookieSet).not.toHaveBeenCalled() - expect(mockGenerateMessengerRedirectUri).toHaveBeenCalledWith( - messengerCredential, - "ws-1", - ) + expect(mockConnectSessionCreate).not.toHaveBeenCalled() + expect(mockListAndAttachCandidates).not.toHaveBeenCalled() + expect(mockResolveOAuthCredential).toHaveBeenCalledWith({ + provider: "messenger", + ownerId: "owner-1", + }) + expect(mockStartSession).toHaveBeenCalledWith({ + workspaceId: "ws-1", + provider: "messenger", + purpose: "connect", + credential: { clientId: "app-id", clientSecret: "app-secret" }, + callbackUrl: "https://app.example.com/integrations/messenger/callback", + actorUserId: "user-1", + platformOwnerId: "owner-1", + }) + expect(mockUpdateReturnUrl).toHaveBeenCalledWith({ + id: "session-2", + returnUrl: "/channels/messenger/select?session=session-2", + }) }) test("404s when the workspace has no messenger credential configured", async () => { diff --git a/apps/builder/__tests__/channels-create-platform-owner.test.ts b/apps/builder/__tests__/channels-create-platform-owner.test.ts index c70eef029b..e90568391e 100644 --- a/apps/builder/__tests__/channels-create-platform-owner.test.ts +++ b/apps/builder/__tests__/channels-create-platform-owner.test.ts @@ -59,12 +59,6 @@ vi.mock( "@/features/integration-instagram/components/instagram-login-select", () => ({ InstagramLoginSelect: () => null }), ) -vi.mock("@/features/integration-instagram/libs/oauth", () => ({ - generateInstagramRedirectUri: vi.fn(async () => ""), -})) -vi.mock("@/features/integration-instagram/libs/oauth-facebook", () => ({ - generateInstagramFacebookRedirectUri: vi.fn(async () => ""), -})) vi.mock("@/features/integration-telegram/components/telegram-connect", () => ({ TelegramConnect: () => null, })) diff --git a/apps/builder/__tests__/channels-create-visibility-guard.test.ts b/apps/builder/__tests__/channels-create-visibility-guard.test.ts index a53a57a5d8..ace55efbdc 100644 --- a/apps/builder/__tests__/channels-create-visibility-guard.test.ts +++ b/apps/builder/__tests__/channels-create-visibility-guard.test.ts @@ -54,12 +54,6 @@ vi.mock( "@/features/integration-instagram/components/instagram-login-select", () => ({ InstagramLoginSelect: () => null }), ) -vi.mock("@/features/integration-instagram/libs/oauth", () => ({ - generateInstagramRedirectUri: vi.fn(async () => ""), -})) -vi.mock("@/features/integration-instagram/libs/oauth-facebook", () => ({ - generateInstagramFacebookRedirectUri: vi.fn(async () => ""), -})) vi.mock("@/features/integration-telegram/components/telegram-connect", () => ({ TelegramConnect: () => null, })) diff --git a/apps/builder/__tests__/channels-instagram-routes.test.ts b/apps/builder/__tests__/channels-instagram-routes.test.ts new file mode 100644 index 0000000000..94a99d0817 --- /dev/null +++ b/apps/builder/__tests__/channels-instagram-routes.test.ts @@ -0,0 +1,198 @@ +// @vitest-environment node + +import { beforeEach, describe, expect, test, vi } from "vitest" + +// --------------------------------------------------------------------------- +// `channels/instagram/route.ts` and `channels/instagram-facebook/route.ts` +// mirror `channels/create/messenger/route.ts`'s non-reuse branch: resolve/ +// create the target workspace up front, start a `ConnectSession`, then set +// its `returnUrl` to the channel's own select page (`?session={id}`) in a +// follow-up call — `startSession` doesn't know the session's own id until +// after it returns, so `returnUrl` can't be passed in up front. Without that +// follow-up call, the OAuth completion would land on the generic +// `/connect/{id}` page instead of the picker's confirm screen. +// --------------------------------------------------------------------------- + +const { + mockFindWorkspaceById, + mockGetCurrentUserId, + mockRedirect, + mockRequireWorkspacePermission, + mockResolveOAuthCredential, + mockStartSession, + mockUpdateReturnUrl, + mockWorkspaceCreate, +} = vi.hoisted(() => ({ + mockFindWorkspaceById: vi.fn(async () => ({ + id: "ws-1", + ownerId: "owner-1", + })), + mockGetCurrentUserId: vi.fn( + async (): Promise => "user-1", + ), + mockRedirect: vi.fn((path: string) => { + throw new Error(`redirect:${path}`) + }), + mockRequireWorkspacePermission: vi.fn(async () => undefined), + mockResolveOAuthCredential: vi.fn(), + mockStartSession: vi.fn(), + mockUpdateReturnUrl: vi.fn(), + mockWorkspaceCreate: vi.fn(async () => ({ id: "ws-new", ownerId: "user-1" })), +})) + +vi.mock("next/navigation", () => ({ + notFound: vi.fn(() => { + throw new Error("not found") + }), + redirect: mockRedirect, +})) + +vi.mock("@chatbotx.io/business", () => ({ + workspaceService: { + findById: mockFindWorkspaceById, + create: mockWorkspaceCreate, + }, +})) + +vi.mock("@chatbotx.io/business/audit", () => ({})) + +vi.mock("@chatbotx.io/business/connect-session", () => ({ + connectSessionService: { updateReturnUrl: mockUpdateReturnUrl }, +})) + +vi.mock("@chatbotx.io/connections", () => ({ + connectionService: { startSession: mockStartSession }, +})) + +vi.mock("@/features/connections/lib/resolve-connect-credential", () => ({ + resolveOAuthCredential: mockResolveOAuthCredential, +})) + +vi.mock("@/lib/platform-credential-owner", () => ({ + resolvePlatformOwnerId: vi.fn(async () => "owner-1"), +})) + +vi.mock("@/lib/auth/require-workspace-permission", () => ({ + requireWorkspacePermission: mockRequireWorkspacePermission, +})) + +vi.mock("@/lib/auth/utils", () => ({ + getCurrentUserId: mockGetCurrentUserId, +})) + +function requestWithWorkspaceId(workspaceId: string | null) { + const url = new URL("http://localhost/channels/instagram") + if (workspaceId) { + url.searchParams.set("workspaceId", workspaceId) + } + return { nextUrl: url } as unknown as { nextUrl: URL } +} + +beforeEach(() => { + vi.clearAllMocks() + mockGetCurrentUserId.mockResolvedValue("user-1") + mockFindWorkspaceById.mockResolvedValue({ id: "ws-1", ownerId: "owner-1" }) + mockResolveOAuthCredential.mockResolvedValue({ + credential: { clientId: "app-id", clientSecret: "app-secret" }, + callbackUrl: "https://app.example.com/integrations/instagram/callback", + }) + mockStartSession.mockResolvedValue({ + session: { id: "session-1" }, + nextAction: { type: "open_url", url: "https://facebook.com/oauth-dialog" }, + }) + mockUpdateReturnUrl.mockResolvedValue({ id: "session-1" }) +}) + +describe.each([ + { + label: "instagram", + routePath: "../src/app/(no-sidebar)/channels/instagram/route", + provider: "instagram", + selectPath: "/channels/instagram/select", + }, + { + label: "instagram-facebook", + routePath: "../src/app/(no-sidebar)/channels/instagram-facebook/route", + provider: "instagramFacebook", + selectPath: "/channels/instagram-facebook/select", + }, +])("GET /channels/$label", ({ routePath, provider, selectPath }) => { + test("starts an OAuth session, points returnUrl at its own select page, and redirects to the provider's authorize URL", async () => { + const { GET } = await import(routePath) + + await expect(GET(requestWithWorkspaceId("ws-1"))).rejects.toThrow( + "redirect:https://facebook.com/oauth-dialog", + ) + + expect(mockRequireWorkspacePermission).toHaveBeenCalledWith( + "ws-1", + "superAdmin", + ) + expect(mockResolveOAuthCredential).toHaveBeenCalledWith({ + provider, + ownerId: "owner-1", + }) + expect(mockStartSession).toHaveBeenCalledWith({ + workspaceId: "ws-1", + provider, + purpose: "connect", + credential: { clientId: "app-id", clientSecret: "app-secret" }, + callbackUrl: "https://app.example.com/integrations/instagram/callback", + actorUserId: "user-1", + platformOwnerId: "owner-1", + }) + expect(mockUpdateReturnUrl).toHaveBeenCalledWith({ + id: "session-1", + returnUrl: `${selectPath}?session=session-1`, + }) + }) + + test("creates a workspace first when there is no workspaceId yet (first channel ever)", async () => { + const { GET } = await import(routePath) + + await expect(GET(requestWithWorkspaceId(null))).rejects.toThrow( + "redirect:https://facebook.com/oauth-dialog", + ) + + expect(mockRequireWorkspacePermission).not.toHaveBeenCalled() + expect(mockWorkspaceCreate).toHaveBeenCalledWith({ + data: { name: "New Workspace", ownerId: "user-1" }, + createdBy: "user-1", + }) + expect(mockStartSession).toHaveBeenCalledWith( + expect.objectContaining({ workspaceId: "ws-new" }), + ) + }) + + test("404s when the owner has no credential configured for the provider", async () => { + mockResolveOAuthCredential.mockResolvedValue(null) + const { GET } = await import(routePath) + + await expect(GET(requestWithWorkspaceId("ws-1"))).rejects.toThrow( + "not found", + ) + expect(mockStartSession).not.toHaveBeenCalled() + }) + + test("404s when the caller is not signed in", async () => { + mockGetCurrentUserId.mockResolvedValue(undefined) + const { GET } = await import(routePath) + + await expect(GET(requestWithWorkspaceId("ws-1"))).rejects.toThrow( + "not found", + ) + expect(mockResolveOAuthCredential).not.toHaveBeenCalled() + }) + + test("throws when startSession returns a non-open_url next action", async () => { + mockStartSession.mockResolvedValue({ + session: { id: "session-1" }, + nextAction: { type: "show_qr", qr: "data:image/png;base64,..." }, + }) + const { GET } = await import(routePath) + + await expect(GET(requestWithWorkspaceId("ws-1"))).rejects.toThrow( + `Unexpected connect next action for ${provider}`, + ) + }) +}) diff --git a/apps/builder/__tests__/channels-public-scope.test.ts b/apps/builder/__tests__/channels-public-scope.test.ts deleted file mode 100644 index 97d6d80c07..0000000000 --- a/apps/builder/__tests__/channels-public-scope.test.ts +++ /dev/null @@ -1,113 +0,0 @@ -// @vitest-environment node - -import { describe, expect, test, vi } from "vitest" - -// Same rationale as integrations-public-scope.test.ts: importing the real -// public routers transitively pulls in `@chatbotx.io/database/client` (opens -// a real `pg.Pool`) and the full `@chatbotx.io/business` barrel (queues, -// redis cache invalidation, audit dispatch). None of that is reachable from -// this test — it only inspects which scope each submodule registered its -// procedures under — so all of it is stubbed to keep the import -// side-effect-free. -vi.mock("@chatbotx.io/database/client", () => { - const proxy: unknown = new Proxy(() => proxy, { get: () => proxy }) - return { db: proxy } -}) - -vi.mock("@chatbotx.io/database/repositories", () => ({ - integrationMessengerRepository: { listPersonasByWorkspaceId: vi.fn() }, -})) - -vi.mock("@chatbotx.io/integration-messenger", () => ({ - selectRegisteredPersonas: vi.fn(() => []), -})) - -vi.mock("@chatbotx.io/business", () => ({ - userPersistentMenuService: {}, - integrationWebchatService: {}, - resolveTenantSettings: vi.fn(), - integrationSmtpService: {}, - messengerIntegrationService: { updateTagSync: vi.fn() }, - zaloIntegrationService: { updateTagSync: vi.fn() }, -})) - -const workspaceTokenAuthAPIForScope = vi.hoisted(() => - vi.fn((_scope: string) => { - const chain = { - route: vi.fn(() => chain), - input: vi.fn(() => chain), - output: vi.fn(() => chain), - errors: vi.fn(() => chain), - handler: vi.fn(() => ({})), - } - return chain - }), -) - -vi.mock("@/orpc", () => ({ workspaceTokenAuthAPIForScope })) - -// Each submodule calls `workspaceTokenAuthAPIForScope` exactly once at import -// time — `packages/utils`' Snowflake ID generator is a process-wide singleton -// that throws on re-init, so every submodule is imported exactly once here -// (no `vi.resetModules()` between them) and the full accumulated call list is -// asserted at the end, per submodule slice. -await import("@/features/user-persistent-menus/api/public") -const userPersistentMenusCallCount = - workspaceTokenAuthAPIForScope.mock.calls.length - -await import("@/features/integration-webchat/api/public") -const webchatsCallCount = workspaceTokenAuthAPIForScope.mock.calls.length - -await import("@/features/integration-smtp/api/public") -const smtpCallCount = workspaceTokenAuthAPIForScope.mock.calls.length - -await import("@/features/personas/api/public") -const personasCallCount = workspaceTokenAuthAPIForScope.mock.calls.length - -await import("@/features/integration-messenger/api/public") -const messengerCallCount = workspaceTokenAuthAPIForScope.mock.calls.length - -await import("@/features/integration-zalo/api/public") -const zaloCallCount = workspaceTokenAuthAPIForScope.mock.calls.length - -const allScopeCalls = workspaceTokenAuthAPIForScope.mock.calls.map( - (call) => call[0], -) - -describe("channels public router scope wiring", () => { - test("user-persistent-menus/api/public.ts registers under the 'channels' scope", () => { - expect(allScopeCalls.slice(0, userPersistentMenusCallCount)).toEqual([ - "channels", - ]) - }) - - test("integration-webchat/api/public.ts registers under the 'channels' scope", () => { - expect( - allScopeCalls.slice(userPersistentMenusCallCount, webchatsCallCount), - ).toEqual(["channels"]) - }) - - test("integration-smtp/api/public.ts registers under the 'channels' scope", () => { - expect(allScopeCalls.slice(webchatsCallCount, smtpCallCount)).toEqual([ - "channels", - ]) - }) - - test("personas/api/public.ts registers under the 'channels' scope", () => { - expect(allScopeCalls.slice(smtpCallCount, personasCallCount)).toEqual([ - "channels", - ]) - }) - - test("integration-messenger/api/public.ts registers under the 'channels' scope", () => { - expect(allScopeCalls.slice(personasCallCount, messengerCallCount)).toEqual([ - "channels", - ]) - }) - - test("integration-zalo/api/public.ts registers under the 'channels' scope", () => { - expect(allScopeCalls.slice(messengerCallCount, zaloCallCount)).toEqual([ - "channels", - ]) - }) -}) diff --git a/apps/builder/__tests__/connect-client.test.ts b/apps/builder/__tests__/connect-client.test.ts index f6ab63750f..abaaa8b670 100644 --- a/apps/builder/__tests__/connect-client.test.ts +++ b/apps/builder/__tests__/connect-client.test.ts @@ -67,7 +67,7 @@ const unknownFailure = { function connectMessenger() { return connectViaApi({ route: messengerRoute, - body: { pageId: "page-1" }, + body: { sessionId: "session-1", pageId: "page-1" }, parse, item, }) @@ -129,7 +129,10 @@ describe("connectViaApi", () => { await expect(connectMessenger()).resolves.toEqual(connected) expect(connectMessengerPageAPI).toHaveBeenCalledTimes(1) - expect(connectMessengerPageAPI).toHaveBeenCalledWith({ pageId: "page-1" }) + expect(connectMessengerPageAPI).toHaveBeenCalledWith({ + sessionId: "session-1", + pageId: "page-1", + }) // No client-side timeout option is passed: `useConnectBatch`'s own // CONNECT_REQUEST_TIMEOUT_MS is the single timeout authority. expect(connectMessengerPageAPI.mock.calls[0]).toHaveLength(1) @@ -192,10 +195,14 @@ describe("the connect routes call their own channel's procedure", () => { connectMessengerPageAPI.mockResolvedValue(connected) await CONNECT_CHANNEL_REGISTRY.messenger.connectRoute.call({ + sessionId: "session-1", pageId: "page-1", }) - expect(connectMessengerPageAPI).toHaveBeenCalledWith({ pageId: "page-1" }) + expect(connectMessengerPageAPI).toHaveBeenCalledWith({ + sessionId: "session-1", + pageId: "page-1", + }) expect(connectInstagramFacebookAccountAPI).not.toHaveBeenCalled() }) @@ -203,10 +210,12 @@ describe("the connect routes call their own channel's procedure", () => { connectInstagramFacebookAccountAPI.mockResolvedValue(connected) await CONNECT_CHANNEL_REGISTRY.instagram.connectRoute.call({ + sessionId: "session-1", igId: "ig-1", }) expect(connectInstagramFacebookAccountAPI).toHaveBeenCalledWith({ + sessionId: "session-1", igId: "ig-1", }) // The other Instagram login has its own procedure — never this one. @@ -216,9 +225,15 @@ describe("the connect routes call their own channel's procedure", () => { test("Instagram direct login", async () => { connectInstagramAccountAPI.mockResolvedValue(connected) - await INSTAGRAM_DIRECT_CONNECT_ROUTE.call({ igId: "ig-1" }) + await INSTAGRAM_DIRECT_CONNECT_ROUTE.call({ + sessionId: "session-1", + igId: "ig-1", + }) - expect(connectInstagramAccountAPI).toHaveBeenCalledWith({ igId: "ig-1" }) + expect(connectInstagramAccountAPI).toHaveBeenCalledWith({ + sessionId: "session-1", + igId: "ig-1", + }) expect(connectInstagramFacebookAccountAPI).not.toHaveBeenCalled() }) diff --git a/apps/builder/__tests__/connect-routes.test.ts b/apps/builder/__tests__/connect-routes.test.ts index 70e973dfba..37c8812dc6 100644 --- a/apps/builder/__tests__/connect-routes.test.ts +++ b/apps/builder/__tests__/connect-routes.test.ts @@ -95,13 +95,14 @@ describe("connect routes", () => { test("the Messenger route passes the signed-in user and the page id, and nothing else", async () => { const result = await call( integrationMessengerConnectAPIs.connectMessengerPageAPI, - { pageId: "page-1" }, + { sessionId: "session-1", pageId: "page-1" }, { context: stubContext }, ) expect(result).toEqual(connectedOutcome) expect(mockConnectMessengerPage).toHaveBeenCalledWith({ userId: "user-1", + sessionId: "session-1", pageId: "page-1", }) }) @@ -109,12 +110,13 @@ describe("connect routes", () => { test("the Instagram-via-Facebook route passes the signed-in user and the account id", async () => { await call( integrationInstagramConnectAPIs.connectInstagramFacebookAccountAPI, - { igId: "ig-1" }, + { sessionId: "session-1", igId: "ig-1" }, { context: stubContext }, ) expect(mockConnectInstagramAccountViaFacebook).toHaveBeenCalledWith({ userId: "user-1", + sessionId: "session-1", igId: "ig-1", }) }) @@ -122,12 +124,13 @@ describe("connect routes", () => { test("the Instagram direct route passes the signed-in user and the account id", async () => { await call( integrationInstagramConnectAPIs.connectInstagramAccountAPI, - { igId: "ig-1" }, + { sessionId: "session-1", igId: "ig-1" }, { context: stubContext }, ) expect(mockConnectInstagramAccount).toHaveBeenCalledWith({ userId: "user-1", + sessionId: "session-1", igId: "ig-1", }) }) @@ -142,7 +145,7 @@ describe("connect routes", () => { await expect( call( integrationMessengerConnectAPIs.connectMessengerPageAPI, - { pageId: "page-1" }, + { sessionId: "session-1", pageId: "page-1" }, { context: stubContext }, ), ).resolves.toEqual(sessionError) @@ -155,7 +158,7 @@ describe("connect routes", () => { const error = await call( integrationMessengerConnectAPIs.connectMessengerPageAPI, - { pageId: "page-1" }, + { sessionId: "session-1", pageId: "page-1" }, { context: stubContext }, ).catch((thrown: { status?: number }) => thrown) @@ -276,7 +279,7 @@ describe("connect routes", () => { await call( integrationMessengerConnectAPIs.connectMessengerPageAPI, - { pageId: "page-1" }, + { sessionId: "session-1", pageId: "page-1" }, { context: { headers: new Headers({ @@ -305,12 +308,12 @@ describe("connect routes", () => { await call( integrationInstagramConnectAPIs.connectInstagramFacebookAccountAPI, - { igId: "ig-1" }, + { sessionId: "session-1", igId: "ig-1" }, { context: stubContext }, ) await call( integrationInstagramConnectAPIs.connectInstagramAccountAPI, - { igId: "ig-1" }, + { sessionId: "session-1", igId: "ig-1" }, { context: stubContext }, ) await call( diff --git a/apps/builder/__tests__/connect-session-message.test.ts b/apps/builder/__tests__/connect-session-message.test.ts new file mode 100644 index 0000000000..72bec15e49 --- /dev/null +++ b/apps/builder/__tests__/connect-session-message.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, test } from "vitest" +import { resolveConnectSessionMessageKind } from "@/app/connect/[sessionId]/resolve-message" + +describe("resolveConnectSessionMessageKind", () => { + test("returns invalid when the session id resolved to nothing", () => { + expect(resolveConnectSessionMessageKind(null)).toBe("invalid") + }) + + test.each([ + "pending", + "authorized", + "awaiting_selection", + ] as const)("treats %s as still-processing (auto-refreshing), not failed", (status) => { + expect(resolveConnectSessionMessageKind(status)).toBe("processing") + }) + + test("returns completed for a completed session", () => { + expect(resolveConnectSessionMessageKind("completed")).toBe("completed") + }) + + test("returns cancelled for a cancelled session", () => { + expect(resolveConnectSessionMessageKind("cancelled")).toBe("cancelled") + }) + + test("returns expired for an expired session", () => { + expect(resolveConnectSessionMessageKind("expired")).toBe("expired") + }) + + test("returns failed for a failed session", () => { + expect(resolveConnectSessionMessageKind("failed")).toBe("failed") + }) + + test("falls back to failed for an unrecognized future status rather than misreporting progress", () => { + expect( + resolveConnectSessionMessageKind( + "some_future_status" as unknown as "failed", + ), + ).toBe("failed") + }) +}) diff --git a/apps/builder/__tests__/connections-public-api.test.ts b/apps/builder/__tests__/connections-public-api.test.ts new file mode 100644 index 0000000000..bfef0c0474 --- /dev/null +++ b/apps/builder/__tests__/connections-public-api.test.ts @@ -0,0 +1,664 @@ +// @vitest-environment node + +import { beforeEach, describe, expect, test, vi } from "vitest" + +type RouteConfig = { + method: string + path: string + summary: string + tags: string[] +} + +type ProcedureHandler = (...args: unknown[]) => unknown + +type CapturedProcedure = { + route: RouteConfig + handler?: ProcedureHandler +} + +const { workspaceTokenAuthAPIForScope, capturedProcedures } = vi.hoisted(() => { + const capturedProcedures: CapturedProcedure[] = [] + + const makeProcedure = (route: RouteConfig) => { + const record: CapturedProcedure = { route } + capturedProcedures.push(record) + + const chain = { + input: vi.fn(() => chain), + output: vi.fn(() => chain), + errors: vi.fn(() => chain), + handler: vi.fn((fn: ProcedureHandler) => { + record.handler = fn + return { handler: fn } + }), + } + return chain + } + + const workspaceTokenAuthAPI = { + route: vi.fn((config: RouteConfig) => makeProcedure(config)), + } + + return { + workspaceTokenAuthAPIForScope: vi.fn( + (_scope: string) => workspaceTokenAuthAPI, + ), + capturedProcedures, + } +}) + +vi.mock("@/orpc", () => ({ workspaceTokenAuthAPIForScope })) + +const mocks = vi.hoisted(() => ({ + list: vi.fn(), + getForWorkspace: vi.fn(), + updateDisplayName: vi.fn(), + listConnectionProviderResources: vi.fn(), + toConnectionResource: vi.fn((row: { id: string }) => ({ + id: row.id, + resource: true, + })), + toConnectSessionResource: vi.fn((row: { id: string }) => ({ + id: row.id, + sessionResource: true, + })), + channelForProvider: vi.fn( + (_provider: string): string | undefined => undefined, + ), + resolveOAuthCredential: vi.fn(), + sanitizeOptionalReturnUrl: vi.fn(async (url?: string) => url), + resolveOwnerForWorkspace: vi.fn(async () => "owner-1"), + resolveChannelPolicy: vi.fn( + async ( + _workspaceId: string, + ): Promise<{ + ownerId: string + creatable: string[] + visibleChannels: string[] + } | null> => null, + ), + findSessionByIdForWorkspace: vi.fn(), + requireSessionByIdForWorkspace: vi.fn(), + submitSessionInput: vi.fn(), + cancelSession: vi.fn(), +})) + +vi.mock("@chatbotx.io/business", () => ({ + connectionStateService: { + list: mocks.list, + getForWorkspace: mocks.getForWorkspace, + updateDisplayName: mocks.updateDisplayName, + }, + platformCredentialService: { resolveForOwner: vi.fn() }, +})) + +vi.mock("@chatbotx.io/business/connect-session", () => ({ + connectSessionService: { + findByIdForWorkspace: mocks.findSessionByIdForWorkspace, + requireByIdForWorkspace: mocks.requireSessionByIdForWorkspace, + submitInput: mocks.submitSessionInput, + cancel: mocks.cancelSession, + }, +})) + +const connectionServiceMocks = vi.hoisted(() => ({ + disconnect: vi.fn(), + refresh: vi.fn(), + verify: vi.fn(), + connectFromCredentials: vi.fn(), + startSession: vi.fn(), + reconnect: vi.fn(), + connectTargets: vi.fn(), +})) + +vi.mock("@chatbotx.io/connections", () => ({ + connectionService: connectionServiceMocks, + CONNECTION_REGISTRY: { + claude: { provider: { strategy: "api_key", kind: "integration" } }, + messenger: { provider: { strategy: "oauth_redirect", kind: "channel" } }, + zalo: { provider: { strategy: "oauth_redirect", kind: "channel" } }, + }, +})) + +class MockChatbotXException extends Error { + code: string + constructor(message: string, code: string) { + super(message) + this.code = code + } +} + +vi.mock("@chatbotx.io/business/errors", () => ({ + ChatbotXException: MockChatbotXException, + notFoundException: (message: string) => + new MockChatbotXException(message, "notFound"), + channelHiddenException: (channel: string) => + new MockChatbotXException(`${channel} hidden`, "channelHidden"), + connectionNotConfiguredException: (provider: string) => + new MockChatbotXException( + `${provider} not configured`, + "connectionNotConfigured", + ), + connectSessionExpiredException: (message: string) => + new MockChatbotXException(message, "connectSessionExpired"), +})) + +vi.mock("../src/features/connections/lib/resolve-provider", () => ({ + toConnectionResource: mocks.toConnectionResource, + listConnectionProviderResources: mocks.listConnectionProviderResources, + channelForProvider: mocks.channelForProvider, +})) + +vi.mock("../src/features/connections/lib/connect-session-resource", () => ({ + toConnectSessionResource: mocks.toConnectSessionResource, +})) + +vi.mock("../src/features/connections/lib/resolve-connect-credential", () => ({ + resolveOAuthCredential: mocks.resolveOAuthCredential, +})) + +vi.mock("@/lib/oauth-referer", () => ({ + sanitizeOptionalReturnUrl: mocks.sanitizeOptionalReturnUrl, +})) + +vi.mock("@/lib/platform-credential-owner", () => ({ + resolveOwnerForWorkspace: mocks.resolveOwnerForWorkspace, +})) + +vi.mock("@/lib/workspace/resolve-visible-channels", () => ({ + resolveChannelPolicy: mocks.resolveChannelPolicy, +})) + +const { + connectionsPublicRouter, + connectionProvidersPublicRouter, + connectSessionsPublicRouter, +} = await import("../src/features/connections/api/public") + +const findProcedure = (method: string, path: string): CapturedProcedure => { + const found = capturedProcedures.find( + (p) => p.route.method === method && p.route.path === path, + ) + if (!found) { + throw new Error(`No procedure captured for ${method} ${path}`) + } + return found +} + +const context = { + workspace: { id: "workspace-1", ownerId: "owner-1" }, + apiToken: { id: "token-1" }, +} + +beforeEach(() => { + vi.clearAllMocks() + mocks.channelForProvider.mockReturnValue(undefined) + mocks.resolveOwnerForWorkspace.mockResolvedValue("owner-1") + mocks.resolveChannelPolicy.mockResolvedValue(null) + mocks.sanitizeOptionalReturnUrl.mockImplementation( + async (url?: string) => url, + ) +}) + +describe("connections public router", () => { + test("registers list/get under /v1/connections and connection-providers under /v1/connection-providers", () => { + expect(() => connectionsPublicRouter).not.toThrow() + expect(() => connectionProvidersPublicRouter).not.toThrow() + expect(() => connectSessionsPublicRouter).not.toThrow() + expect(findProcedure("GET", "/v1/connections")).toBeDefined() + expect(findProcedure("GET", "/v1/connections/{id}")).toBeDefined() + expect(findProcedure("GET", "/v1/connection-providers")).toBeDefined() + }) +}) + +describe("GET /v1/connections", () => { + const procedure = findProcedure("GET", "/v1/connections") + + test("passes filters through and computes pageCount from the total count", async () => { + mocks.list.mockResolvedValueOnce({ + data: [{ id: "conn-1" }, { id: "conn-2" }], + count: 45, + }) + + const result = await procedure.handler?.({ + context, + input: { + kind: "channel", + provider: "messenger", + page: 1, + perPage: 20, + }, + }) + + expect(mocks.list).toHaveBeenCalledWith({ + workspaceId: "workspace-1", + kind: "channel", + provider: "messenger", + channel: undefined, + status: undefined, + page: 1, + perPage: 20, + }) + expect(result).toEqual({ + data: [ + { id: "conn-1", resource: true }, + { id: "conn-2", resource: true }, + ], + pageCount: 3, + }) + }) + + test("never returns fewer than 1 page even when count is 0", async () => { + mocks.list.mockResolvedValueOnce({ data: [], count: 0 }) + + const result = await procedure.handler?.({ + context, + input: { page: 1, perPage: 20 }, + }) + + expect((result as { pageCount: number }).pageCount).toBe(1) + }) +}) + +describe("GET /v1/connections/{id}", () => { + const procedure = findProcedure("GET", "/v1/connections/{id}") + + test("throws notFoundException when the connection does not exist in this workspace", async () => { + mocks.getForWorkspace.mockResolvedValueOnce(undefined) + + await expect( + procedure.handler?.({ context, input: { id: "missing" } }), + ).rejects.toThrow("Connection not found") + }) + + test("returns the resolved resource, scoped to the token's workspace", async () => { + mocks.getForWorkspace.mockResolvedValueOnce({ id: "conn-1" }) + + const result = await procedure.handler?.({ + context, + input: { id: "conn-1" }, + }) + + expect(mocks.getForWorkspace).toHaveBeenCalledWith({ + id: "conn-1", + workspaceId: "workspace-1", + }) + expect(result).toEqual({ id: "conn-1", resource: true }) + }) +}) + +describe("GET /v1/connection-providers", () => { + const procedure = findProcedure("GET", "/v1/connection-providers") + + test("delegates to listConnectionProviderResources scoped to the token's workspace", async () => { + mocks.listConnectionProviderResources.mockResolvedValueOnce([ + { provider: "messenger", available: true }, + ]) + + const result = await procedure.handler?.({ + context, + input: { kind: "channel" }, + }) + + expect(mocks.listConnectionProviderResources).toHaveBeenCalledWith({ + workspaceId: "workspace-1", + kind: "channel", + }) + expect(result).toEqual({ + data: [{ provider: "messenger", available: true }], + pageCount: 1, + }) + }) +}) + +describe("DELETE /v1/connections/{id}", () => { + const procedure = findProcedure("DELETE", "/v1/connections/{id}") + + test("delegates to connectionService.disconnect scoped to the token's workspace", async () => { + connectionServiceMocks.disconnect.mockResolvedValueOnce({ id: "conn-1" }) + + const result = await procedure.handler?.({ + context, + input: { id: "conn-1" }, + }) + + expect(connectionServiceMocks.disconnect).toHaveBeenCalledWith({ + connectionId: "conn-1", + workspaceId: "workspace-1", + }) + expect(result).toEqual({ id: "conn-1", resource: true }) + }) +}) + +describe("POST /v1/connections/{id}/refresh", () => { + const procedure = findProcedure("POST", "/v1/connections/{id}/refresh") + + test("delegates to connectionService.refresh scoped to the token's workspace", async () => { + connectionServiceMocks.refresh.mockResolvedValueOnce({ id: "conn-1" }) + + const result = await procedure.handler?.({ + context, + input: { id: "conn-1" }, + }) + + expect(connectionServiceMocks.refresh).toHaveBeenCalledWith({ + connectionId: "conn-1", + workspaceId: "workspace-1", + }) + expect(result).toEqual({ id: "conn-1", resource: true }) + }) + + test("propagates a connectionInactive failure without swallowing it", async () => { + connectionServiceMocks.refresh.mockRejectedValueOnce( + new MockChatbotXException( + "This connection is not active.", + "connectionInactive", + ), + ) + + await expect( + procedure.handler?.({ context, input: { id: "conn-1" } }), + ).rejects.toThrow("This connection is not active.") + }) +}) + +describe("POST /v1/connections/{id}/verify", () => { + const procedure = findProcedure("POST", "/v1/connections/{id}/verify") + + test("delegates to connectionService.verify scoped to the token's workspace", async () => { + connectionServiceMocks.verify.mockResolvedValueOnce({ id: "conn-1" }) + + const result = await procedure.handler?.({ + context, + input: { id: "conn-1" }, + }) + + expect(connectionServiceMocks.verify).toHaveBeenCalledWith({ + connectionId: "conn-1", + workspaceId: "workspace-1", + }) + expect(result).toEqual({ id: "conn-1", resource: true }) + }) +}) + +describe("POST /v1/connections", () => { + const procedure = findProcedure("POST", "/v1/connections") + + test("connects a credential-strategy provider immediately and returns connection, session: null", async () => { + mocks.channelForProvider.mockReturnValue(undefined) + connectionServiceMocks.connectFromCredentials.mockResolvedValueOnce({ + id: "conn-1", + }) + + const result = await procedure.handler?.({ + context, + input: { provider: "claude", config: { apiKey: "sk-live" } }, + }) + + expect(connectionServiceMocks.connectFromCredentials).toHaveBeenCalledWith({ + workspaceId: "workspace-1", + provider: "claude", + config: { apiKey: "sk-live" }, + }) + expect(result).toEqual({ + connection: { id: "conn-1", resource: true }, + session: null, + }) + expect(connectionServiceMocks.startSession).not.toHaveBeenCalled() + }) + + test("starts an OAuth session and returns connection: null, session", async () => { + mocks.channelForProvider.mockReturnValue("messenger") + mocks.list.mockResolvedValueOnce({ data: [], count: 0 }) + mocks.resolveChannelPolicy.mockResolvedValueOnce({ + ownerId: "owner-1", + creatable: ["messenger"], + visibleChannels: ["messenger"], + }) + mocks.resolveOAuthCredential.mockResolvedValueOnce({ + credential: { clientId: "id" }, + callbackUrl: "https://app.example.test/integrations/messenger/callback", + }) + connectionServiceMocks.startSession.mockResolvedValueOnce({ + session: { id: "session-1" }, + }) + + const result = await procedure.handler?.({ + context, + input: { provider: "messenger" }, + }) + + expect(connectionServiceMocks.startSession).toHaveBeenCalledWith( + expect.objectContaining({ + workspaceId: "workspace-1", + provider: "messenger", + purpose: "connect", + actorTokenId: "token-1", + platformOwnerId: "owner-1", + }), + ) + expect(result).toEqual({ + connection: null, + session: { id: "session-1", sessionResource: true }, + }) + }) + + test("throws channelHidden when the channel is hidden and not already connected", async () => { + mocks.channelForProvider.mockReturnValue("messenger") + mocks.list.mockResolvedValueOnce({ data: [], count: 0 }) + mocks.resolveChannelPolicy.mockResolvedValueOnce({ + ownerId: "owner-1", + creatable: [], + visibleChannels: [], + }) + + await expect( + procedure.handler?.({ context, input: { provider: "messenger" } }), + ).rejects.toMatchObject({ code: "channelHidden" }) + expect(connectionServiceMocks.startSession).not.toHaveBeenCalled() + }) + + test("skips the hidden-channel check when the provider is already connected in this workspace", async () => { + mocks.channelForProvider.mockReturnValue("messenger") + mocks.list.mockResolvedValueOnce({ + data: [{ id: "conn-existing" }], + count: 1, + }) + mocks.resolveOAuthCredential.mockResolvedValueOnce({ + credential: {}, + callbackUrl: "https://app.example.test/callback", + }) + connectionServiceMocks.startSession.mockResolvedValueOnce({ + session: { id: "session-1" }, + }) + + await procedure.handler?.({ context, input: { provider: "messenger" } }) + + expect(mocks.resolveChannelPolicy).not.toHaveBeenCalled() + }) +}) + +describe("POST /v1/connections/{id}/reconnect", () => { + const procedure = findProcedure("POST", "/v1/connections/{id}/reconnect") + + test("throws notFound when the connection does not exist", async () => { + mocks.getForWorkspace.mockResolvedValueOnce(undefined) + await expect( + procedure.handler?.({ context, input: { id: "missing" } }), + ).rejects.toMatchObject({ code: "notFound" }) + }) + + test("starts a reconnect session for the connection's own provider", async () => { + mocks.getForWorkspace.mockResolvedValueOnce({ + id: "conn-1", + provider: "zalo", + }) + mocks.resolveOAuthCredential.mockResolvedValueOnce({ + credential: { appId: "id" }, + callbackUrl: "https://app.example.test/integrations/zalo/callback", + }) + connectionServiceMocks.reconnect.mockResolvedValueOnce({ + session: { id: "session-2" }, + }) + + const result = await procedure.handler?.({ + context, + input: { id: "conn-1" }, + }) + + expect(connectionServiceMocks.reconnect).toHaveBeenCalledWith( + expect.objectContaining({ + connectionId: "conn-1", + workspaceId: "workspace-1", + actorTokenId: "token-1", + platformOwnerId: "owner-1", + }), + ) + expect(result).toEqual({ + connection: null, + session: { id: "session-2", sessionResource: true }, + }) + }) +}) + +describe("PATCH /v1/connections/{id}", () => { + const procedure = findProcedure("PATCH", "/v1/connections/{id}") + + test("throws notFound when the connection does not exist in this workspace", async () => { + mocks.updateDisplayName.mockResolvedValueOnce(undefined) + await expect( + procedure.handler?.({ + context, + input: { id: "missing", displayName: "New name" }, + }), + ).rejects.toMatchObject({ code: "notFound" }) + }) + + test("renames the connection scoped to the token's workspace", async () => { + mocks.updateDisplayName.mockResolvedValueOnce({ id: "conn-1" }) + + const result = await procedure.handler?.({ + context, + input: { id: "conn-1", displayName: "New name" }, + }) + + expect(mocks.updateDisplayName).toHaveBeenCalledWith({ + id: "conn-1", + workspaceId: "workspace-1", + displayName: "New name", + }) + expect(result).toEqual({ id: "conn-1", resource: true }) + }) +}) + +describe("GET /v1/connect-sessions/{id}", () => { + const procedure = () => findProcedure("GET", "/v1/connect-sessions/{id}") + + test("throws notFound when the session does not exist in this workspace", async () => { + mocks.findSessionByIdForWorkspace.mockResolvedValueOnce(undefined) + await expect( + procedure().handler?.({ context, input: { id: "missing" } }), + ).rejects.toMatchObject({ code: "notFound" }) + }) + + test("returns the session resource scoped to the token's workspace", async () => { + mocks.findSessionByIdForWorkspace.mockResolvedValueOnce({ id: "session-1" }) + const result = await procedure().handler?.({ + context, + input: { id: "session-1" }, + }) + expect(mocks.findSessionByIdForWorkspace).toHaveBeenCalledWith({ + id: "session-1", + workspaceId: "workspace-1", + }) + expect(result).toEqual({ id: "session-1", sessionResource: true }) + }) +}) + +describe("POST /v1/connect-sessions/{id}/targets", () => { + const procedure = () => + findProcedure("POST", "/v1/connect-sessions/{id}/targets") + + test("delegates to connectionService.connectTargets and maps the envelope", async () => { + connectionServiceMocks.connectTargets.mockResolvedValueOnce({ + session: { id: "session-1" }, + connections: [{ id: "conn-1" }], + outcomes: [ + { targetId: "page-1", status: "connected", connectionId: "conn-1" }, + ], + }) + + const result = await procedure().handler?.({ + context, + input: { id: "session-1", targetIds: ["page-1"] }, + }) + + expect(connectionServiceMocks.connectTargets).toHaveBeenCalledWith({ + sessionId: "session-1", + workspaceId: "workspace-1", + targetIds: ["page-1"], + }) + expect(result).toEqual({ + session: { id: "session-1", sessionResource: true }, + connections: [{ id: "conn-1", resource: true }], + outcomes: [ + { targetId: "page-1", status: "connected", connectionId: "conn-1" }, + ], + }) + }) +}) + +describe("POST /v1/connect-sessions/{id}/input", () => { + const procedure = () => + findProcedure("POST", "/v1/connect-sessions/{id}/input") + + test("throws connectSessionExpired when the session is not awaiting input", async () => { + mocks.requireSessionByIdForWorkspace.mockResolvedValueOnce({ + id: "session-1", + nextAction: { type: "open_url", url: "https://x" }, + }) + await expect( + procedure().handler?.({ + context, + input: { id: "session-1", input: { pin: "123456" } }, + }), + ).rejects.toMatchObject({ code: "connectSessionExpired" }) + expect(mocks.submitSessionInput).not.toHaveBeenCalled() + }) + + test("acknowledges an enter_input session and reverts to wait", async () => { + mocks.requireSessionByIdForWorkspace.mockResolvedValueOnce({ + id: "session-1", + nextAction: { type: "enter_input", inputFields: [] }, + }) + mocks.submitSessionInput.mockResolvedValueOnce({ id: "session-1" }) + + const result = await procedure().handler?.({ + context, + input: { id: "session-1", input: { pin: "123456" } }, + }) + + expect(mocks.submitSessionInput).toHaveBeenCalledWith({ + id: "session-1", + nextAction: { type: "wait" }, + }) + expect(result).toEqual({ id: "session-1", sessionResource: true }) + }) +}) + +describe("DELETE /v1/connect-sessions/{id}", () => { + const procedure = () => findProcedure("DELETE", "/v1/connect-sessions/{id}") + + test("delegates to connectSessionService.cancel scoped to the token's workspace", async () => { + mocks.cancelSession.mockResolvedValueOnce({ id: "session-1" }) + const result = await procedure().handler?.({ + context, + input: { id: "session-1" }, + }) + expect(mocks.cancelSession).toHaveBeenCalledWith({ + id: "session-1", + workspaceId: "workspace-1", + }) + expect(result).toEqual({ id: "session-1", sessionResource: true }) + }) +}) diff --git a/apps/builder/__tests__/connections-public-scope.test.ts b/apps/builder/__tests__/connections-public-scope.test.ts new file mode 100644 index 0000000000..196c5de048 --- /dev/null +++ b/apps/builder/__tests__/connections-public-scope.test.ts @@ -0,0 +1,258 @@ +// @vitest-environment node + +import { describe, expect, test, vi } from "vitest" + +// Importing the real public routers transitively pulls in +// `@chatbotx.io/database/client` (opens a real `pg.Pool`), the full +// `@chatbotx.io/business` barrel (queues, redis cache invalidation, audit +// dispatch), and `@/orpc`'s `authorizedAPI` chain (boots the full better-auth +// stack via `@/middlewares/auth`). None of that is reachable from this test — +// it only inspects which scope each submodule registered its procedures +// under — so all of it is stubbed to keep the import side-effect-free. +vi.mock("@/middlewares/auth", () => ({ + authMiddleware: vi.fn(), + workspaceAuthorizedMidddleware: vi.fn(), +})) + +vi.mock("@chatbotx.io/database/client", () => { + const proxy: unknown = new Proxy(() => proxy, { get: () => proxy }) + return { db: proxy } +}) + +vi.mock("@chatbotx.io/database/repositories", () => ({ + integrationMessengerRepository: { listPersonasByWorkspaceId: vi.fn() }, + connectionRepository: { findByProviderSourceId: vi.fn() }, +})) + +vi.mock("@chatbotx.io/connections", () => ({ + connectionService: { + disconnect: vi.fn(), + connectFromCredentials: vi.fn(), + startSession: vi.fn(), + reconnect: vi.fn(), + connectTargets: vi.fn(), + }, + CONNECTION_REGISTRY: {}, +})) + +vi.mock("@chatbotx.io/integration-messenger", () => ({ + selectRegisteredPersonas: vi.fn(() => []), +})) + +vi.mock("@chatbotx.io/business", () => ({ + userPersistentMenuService: {}, + integrationWebchatService: {}, + resolveTenantSettings: vi.fn(), + integrationSmtpService: {}, + messengerIntegrationService: { updateTagSync: vi.fn() }, + zaloIntegrationService: { updateTagSync: vi.fn() }, + integrationService: {}, + webhookService: {}, + externalWebhookService: {}, + integrationClaudeService: {}, + integrationDeepSeekService: {}, + integrationGeminiService: {}, + integrationOpenAIService: {}, + connectionStateService: {}, + platformCredentialService: { resolveForOwner: vi.fn() }, +})) + +vi.mock("@chatbotx.io/business/connect-session", () => ({ + connectSessionService: { + findByIdForWorkspace: vi.fn(), + requireByIdForWorkspace: vi.fn(), + submitInput: vi.fn(), + cancel: vi.fn(), + }, +})) + +vi.mock("@/features/connections/lib/resolve-connect-credential", () => ({ + resolveOAuthCredential: vi.fn(), +})) + +vi.mock("@/features/connections/lib/connect-session-resource", () => ({ + toConnectSessionResource: vi.fn(), +})) + +vi.mock("@/lib/oauth-referer", () => ({ + sanitizeOptionalReturnUrl: vi.fn(), +})) + +vi.mock("@/lib/platform-credential-owner", () => ({ + resolveOwnerForWorkspace: vi.fn(), + resolvePlatformOwnerId: vi.fn(), +})) + +vi.mock("@/lib/workspace/resolve-visible-channels", () => ({ + resolveChannelPolicy: vi.fn(), +})) + +vi.mock("@/features/connections/lib/resolve-provider", () => ({ + toConnectionResource: vi.fn(), + listConnectionProviderResources: vi.fn(), + channelForProvider: vi.fn(), +})) + +vi.mock("@chatbotx.io/business/errors", () => { + class MockChatbotXException extends Error { + code: string + constructor(message: string, code: string) { + super(message) + this.code = code + } + } + return { + ChatbotXException: MockChatbotXException, + notFoundException: vi.fn(), + validationException: vi.fn(), + channelHiddenException: vi.fn(), + connectionNotConfiguredException: vi.fn(), + connectSessionExpiredException: vi.fn(), + } +}) + +vi.mock("@chatbotx.io/ai", () => ({ + aiProviders: { + enum: { + claude: "claude", + deepseek: "deepseek", + gemini: "gemini", + openai: "openai", + }, + }, +})) + +vi.mock("@chatbotx.io/ai/server", () => ({ + aiIntegrationService: {}, +})) + +vi.mock("@chatbotx.io/business/integration-ai-provider/verify", () => ({ + verifyAiProviderApiKey: vi.fn(), +})) + +const workspaceTokenAuthAPIForScope = vi.hoisted(() => + vi.fn((_scope: string) => { + const chain = { + route: vi.fn(() => chain), + input: vi.fn(() => chain), + output: vi.fn(() => chain), + errors: vi.fn(() => chain), + handler: vi.fn(() => ({})), + } + return chain + }), +) + +vi.mock("@/orpc", () => ({ workspaceTokenAuthAPIForScope })) + +// Each submodule calls `workspaceTokenAuthAPIForScope` exactly once at import +// time — `packages/utils`' Snowflake ID generator is a process-wide singleton +// that throws on re-init, so every submodule is imported exactly once here +// (no `vi.resetModules()` between them) and the full accumulated call list is +// asserted at the end, per submodule slice. +await import("@/features/user-persistent-menus/api/public") +const userPersistentMenusCallCount = + workspaceTokenAuthAPIForScope.mock.calls.length + +await import("@/features/integration-webchat/api/public") +const webchatsCallCount = workspaceTokenAuthAPIForScope.mock.calls.length + +await import("@/features/integration-smtp/api/public") +const smtpCallCount = workspaceTokenAuthAPIForScope.mock.calls.length + +await import("@/features/personas/api/public") +const personasCallCount = workspaceTokenAuthAPIForScope.mock.calls.length + +await import("@/features/integration-messenger/api/public") +const messengerCallCount = workspaceTokenAuthAPIForScope.mock.calls.length + +await import("@/features/integration-zalo/api/public") +const zaloCallCount = workspaceTokenAuthAPIForScope.mock.calls.length + +await import("@/features/integrations/api/public/crud") +const crudCallCount = workspaceTokenAuthAPIForScope.mock.calls.length + +await import("@/features/integrations/api/public/ai") +const aiCallCount = workspaceTokenAuthAPIForScope.mock.calls.length + +await import("@/features/webhooks/api/public") +const webhooksCallCount = workspaceTokenAuthAPIForScope.mock.calls.length + +await import("@/features/external-webhooks/api/public") +const externalWebhooksCallCount = + workspaceTokenAuthAPIForScope.mock.calls.length + +await import("@/features/connections/api/public") +const connectionsCallCount = workspaceTokenAuthAPIForScope.mock.calls.length + +const allScopeCalls = workspaceTokenAuthAPIForScope.mock.calls.map( + (call) => call[0], +) + +describe("connections public router scope wiring", () => { + test("user-persistent-menus/api/public.ts registers under the 'connections' scope", () => { + expect(allScopeCalls.slice(0, userPersistentMenusCallCount)).toEqual([ + "connections", + ]) + }) + + test("integration-webchat/api/public.ts registers under the 'connections' scope", () => { + expect( + allScopeCalls.slice(userPersistentMenusCallCount, webchatsCallCount), + ).toEqual(["connections"]) + }) + + test("integration-smtp/api/public.ts registers under the 'connections' scope", () => { + expect(allScopeCalls.slice(webchatsCallCount, smtpCallCount)).toEqual([ + "connections", + ]) + }) + + test("personas/api/public.ts registers under the 'connections' scope", () => { + expect(allScopeCalls.slice(smtpCallCount, personasCallCount)).toEqual([ + "connections", + ]) + }) + + test("integration-messenger/api/public.ts registers under the 'connections' scope", () => { + expect(allScopeCalls.slice(personasCallCount, messengerCallCount)).toEqual([ + "connections", + ]) + }) + + test("integration-zalo/api/public.ts registers under the 'connections' scope", () => { + expect(allScopeCalls.slice(messengerCallCount, zaloCallCount)).toEqual([ + "connections", + ]) + }) + + test("integrations/api/public/crud.ts registers under the 'connections' scope", () => { + expect(allScopeCalls.slice(zaloCallCount, crudCallCount)).toEqual([ + "connections", + ]) + }) + + test("integrations/api/public/ai.ts registers under the 'connections' scope", () => { + expect(allScopeCalls.slice(crudCallCount, aiCallCount)).toEqual([ + "connections", + ]) + }) + + test("webhooks/api/public.ts registers under the 'connections' scope", () => { + expect(allScopeCalls.slice(aiCallCount, webhooksCallCount)).toEqual([ + "connections", + ]) + }) + + test("external-webhooks/api/public.ts registers under the 'connections' scope", () => { + expect( + allScopeCalls.slice(webhooksCallCount, externalWebhooksCallCount), + ).toEqual(["connections"]) + }) + + test("connections/api/public.ts registers under the 'connections' scope", () => { + expect( + allScopeCalls.slice(externalWebhooksCallCount, connectionsCallCount), + ).toEqual(["connections"]) + }) +}) diff --git a/apps/builder/__tests__/connections-public-spec-flat.test.ts b/apps/builder/__tests__/connections-public-spec-flat.test.ts new file mode 100644 index 0000000000..5b9cf4ca89 --- /dev/null +++ b/apps/builder/__tests__/connections-public-spec-flat.test.ts @@ -0,0 +1,119 @@ +// @vitest-environment node + +import { OpenAPIGenerator } from "@orpc/openapi" +import { ZodToJsonSchemaConverter } from "@orpc/zod/zod4" +import { beforeAll, describe, expect, test, vi } from "vitest" + +// Same import-side-effect stubs as `public-spec-operations.test.ts` — this +// test only inspects generated route metadata, never calls a handler. +vi.mock("@/middlewares/auth", () => ({ + authMiddleware: vi.fn(), + workspaceAuthorizedMidddleware: vi.fn(), +})) + +vi.mock("@chatbotx.io/database/client", () => { + const proxy: unknown = new Proxy(() => proxy, { get: () => proxy }) + return { db: proxy } +}) + +type OpenApiOperation = { + operationId?: string + requestBody?: { content?: Record } +} + +type JsonSchema = { + properties?: Record + oneOf?: JsonSchema[] + anyOf?: JsonSchema[] + allOf?: JsonSchema[] + $ref?: string +} + +const CONNECTIONS_PATH_PATTERN = + /^\/v1\/(connections|connection-providers|connect-sessions)(\/|$)/ + +let postOperationsUnderTest: { + operationId: string + method: string + path: string + bodySchema?: JsonSchema +}[] + +beforeAll(async () => { + const { publicRouter } = await import("@/routers/public") + const { publicSpecGenerateOptions, withChannelApiTokenSecurity } = + await import("@/lib/orpc/public-spec") + + const generator = new OpenAPIGenerator({ + schemaConverters: [new ZodToJsonSchemaConverter()], + }) + + const spec = withChannelApiTokenSecurity( + await generator.generate( + publicRouter, + publicSpecGenerateOptions("connections-public-spec-flat.test"), + ), + ) + + postOperationsUnderTest = [] + for (const [path, methods] of Object.entries(spec.paths ?? {})) { + if (!CONNECTIONS_PATH_PATTERN.test(path)) { + continue + } + for (const [method, operation] of Object.entries( + methods as Record, + )) { + if (!operation.operationId) { + continue + } + postOperationsUnderTest.push({ + operationId: operation.operationId, + method: method.toUpperCase(), + path, + bodySchema: + operation.requestBody?.content?.["application/json"]?.schema, + }) + } + } +}, 120_000) + +describe("public API spec — connections/connect-sessions request bodies stay MCP-flat", () => { + test("no request body under /v1/connections*, /v1/connection-providers, or /v1/connect-sessions* uses oneOf/anyOf/allOf", () => { + const nonFlat = postOperationsUnderTest + .filter((op) => op.bodySchema) + .filter((op) => { + const schema = op.bodySchema as JsonSchema + return Boolean(schema.oneOf || schema.anyOf || schema.allOf) + }) + .map((op) => op.operationId) + + expect(nonFlat).toEqual([]) + }) + + test("every POST body has a flat top-level `properties` object (what MCP's buildInputSchema reads)", () => { + const missingProperties = postOperationsUnderTest + .filter((op) => op.method === "POST" && op.bodySchema) + .filter((op) => !(op.bodySchema as JsonSchema).properties) + .map((op) => op.operationId) + + expect(missingProperties).toEqual([]) + }) + + test("covers the expected connections + connect-sessions POST operations (sanity: the filter above is not vacuously empty)", () => { + const postOperationIds = postOperationsUnderTest + .filter((op) => op.method === "POST") + .map((op) => op.operationId) + .sort() + + expect(postOperationIds).toEqual( + [ + "connectSessions.connectTargets", + "connectSessions.submitInput", + "connections.create", + "connections.reconnect", + "connections.refresh", + "connections.verify", + ].sort(), + ) + }) +}) diff --git a/apps/builder/__tests__/facebook-pending-auth.cookie-store.test.ts b/apps/builder/__tests__/facebook-pending-auth.cookie-store.test.ts deleted file mode 100644 index 51ef1e08ad..0000000000 --- a/apps/builder/__tests__/facebook-pending-auth.cookie-store.test.ts +++ /dev/null @@ -1,243 +0,0 @@ -// @vitest-environment node - -import { readdirSync } from "node:fs" -import { fileURLToPath } from "node:url" -// This is the class `cookies()` hands a Server Action / Route Handler: -// `next/dist/server/request/cookies.js` builds its mutable store from -// `MutableRequestCookiesAdapter.wrap(...)`, which wraps a -// `new ResponseCookies(new Headers())` taken from -// `next/dist/server/web/spec-extension/cookies` — itself a re-export of -// `next/dist/compiled/@edge-runtime/cookies`. Importing it here means these -// tests observe production serialization, not a `vi.fn()` shaped like it. -import { ResponseCookies } from "next/dist/server/web/spec-extension/cookies" -import { describe, expect, test, vi } from "vitest" -import { CONNECT_RETRY_HREF } from "@/features/channel-connect/lib/registry" -import { - FB_INSTAGRAM_FACEBOOK_PENDING_AUTH_COOKIE, - FB_INSTAGRAM_PENDING_AUTH_COOKIE, - FB_MESSENGER_PENDING_AUTH_COOKIE, - FB_PENDING_AUTH_MAX_AGE, - writePendingAuth, -} from "@/lib/facebook-pending-auth" -import { RPC_ENDPOINT_PATH } from "@/lib/orpc/orpc" - -const PENDING_AUTH_COOKIES = [ - FB_MESSENGER_PENDING_AUTH_COOKIE, - FB_INSTAGRAM_PENDING_AUTH_COOKIE, - FB_INSTAGRAM_FACEBOOK_PENDING_AUTH_COOKIE, -] as const - -/** A fresh store over its own `Headers`, plus the raw `Set-Cookie` lines it emits. */ -function responseCookieStore() { - const headers = new Headers() - return { - store: new ResponseCookies(headers), - setCookieHeaders: () => headers.getSetCookie(), - } -} - -/** The `Set-Cookie` lines whose cookie-name is `name`. */ -function setCookiesFor(headers: string[], name: string) { - return headers.filter((header) => header.startsWith(`${name}=`)) -} - -/** Attributes of one `Set-Cookie` line, lower-cased keys, `""` for valueless flags. */ -function attributesOf(setCookie: string): Record { - const [, ...rest] = setCookie.split("; ") - return Object.fromEntries( - rest.map((part) => { - const separator = part.indexOf("=") - return separator === -1 - ? [part.toLowerCase(), ""] - : [part.slice(0, separator).toLowerCase(), part.slice(separator + 1)] - }), - ) -} - -// --------------------------------------------------------------------------- -// 1. The store itself. `writePendingAuth` is exercised against Next's real -// `ResponseCookies`, so what is asserted is the header the browser gets. -// --------------------------------------------------------------------------- - -describe("writePendingAuth against Next's real ResponseCookies", () => { - test.each( - PENDING_AUTH_COOKIES, - )("emits exactly one Set-Cookie for %s", (cookieName) => { - const { store, setCookieHeaders } = responseCookieStore() - - writePendingAuth(store, cookieName, "token-1") - - expect(setCookiesFor(setCookieHeaders(), cookieName)).toHaveLength(1) - }) - - test("serializes the pending-auth cookie with the options the connect routes need", () => { - const { store, setCookieHeaders } = responseCookieStore() - - writePendingAuth(store, FB_MESSENGER_PENDING_AUTH_COOKIE, "token-1") - - const [setCookie] = setCookiesFor( - setCookieHeaders(), - FB_MESSENGER_PENDING_AUTH_COOKIE, - ) - expect(setCookie).toBeDefined() - expect( - setCookie.startsWith(`${FB_MESSENGER_PENDING_AUTH_COOKIE}=token-1;`), - ).toBe(true) - - const attributes = attributesOf(setCookie as string) - expect(attributes).toMatchObject({ - path: "/", - "max-age": String(FB_PENDING_AUTH_MAX_AGE), - httponly: "", - samesite: "lax", - }) - // Not production here, so the cookie must not claim `Secure` — a `Secure` - // cookie is dropped outright over plain-http local dev. - expect(attributes).not.toHaveProperty("secure") - }) - - test("marks the serialized cookie Secure in production", () => { - vi.stubEnv("NODE_ENV", "production") - const { store, setCookieHeaders } = responseCookieStore() - - writePendingAuth(store, FB_MESSENGER_PENDING_AUTH_COOKIE, "token-1") - - const [setCookie] = setCookiesFor( - setCookieHeaders(), - FB_MESSENGER_PENDING_AUTH_COOKIE, - ) - expect(attributesOf(setCookie as string)).toHaveProperty("secure", "") - vi.unstubAllEnvs() - }) - - /** - * The trap this module's doc comment names, pinned against the real class: - * `ResponseCookies` keys its parsed map by cookie NAME, so "write the new - * cookie, then expire the old picker-scoped one under the same name" does - * not produce two headers — the second `set()` silently replaces the first - * and the browser receives only the expiry. Any future edit that adds a - * second `set()` for one name must fail here. - */ - test("a second set() under one name replaces the first instead of adding a header", () => { - const { store, setCookieHeaders } = responseCookieStore() - - store.set(FB_MESSENGER_PENDING_AUTH_COOKIE, "token-1", { - path: "/", - maxAge: FB_PENDING_AUTH_MAX_AGE, - }) - store.set(FB_MESSENGER_PENDING_AUTH_COOKIE, "", { - path: "/channels/messenger/select", - maxAge: 0, - }) - - const headers = setCookiesFor( - setCookieHeaders(), - FB_MESSENGER_PENDING_AUTH_COOKIE, - ) - expect(headers).toHaveLength(1) - expect(attributesOf(headers[0] as string)).toMatchObject({ - path: "/channels/messenger/select", - "max-age": "0", - }) - }) -}) - -// --------------------------------------------------------------------------- -// 2. The cookie's `Path` against every path that has to receive it. -// --------------------------------------------------------------------------- - -/** - * RFC 6265 §5.1.4, verbatim. A request-path path-matches a cookie-path if: - * - * 1. the cookie-path and the request-path are identical; or - * 2. the cookie-path is a prefix of the request-path and the last character - * of the cookie-path is `/`; or - * 3. the cookie-path is a prefix of the request-path and the first character - * of the request-path not included in the cookie-path is `/`. - * - * Written out rather than reduced to `startsWith` because the difference is - * exactly the bug this file guards: `/channels/messenger/select` does NOT - * path-match a request to the `/rpc` connect endpoint. - */ -function pathMatches(requestPath: string, cookiePath: string): boolean { - if (requestPath === cookiePath) { - return true - } - if (!requestPath.startsWith(cookiePath)) { - return false - } - if (cookiePath.endsWith("/")) { - return true - } - return requestPath.charAt(cookiePath.length) === "/" -} - -/** The picker pages, read off the route tree so a new picker is covered on sight. */ -function pickerRoutes(): string[] { - const channelsDir = fileURLToPath( - new URL("../src/app/(no-sidebar)/channels", import.meta.url), - ) - return readdirSync(channelsDir, { withFileTypes: true }) - .filter((entry) => entry.isDirectory()) - .flatMap((entry) => { - const children = readdirSync(`${channelsDir}/${entry.name}`, { - withFileTypes: true, - }) - return children.some( - (child) => child.isDirectory() && child.name === "select", - ) - ? [`/channels/${entry.name}/select`] - : [] - }) -} - -/** `writePendingAuth`'s serialized `Path`, read back off a real Set-Cookie header. */ -function writtenCookiePath(): string { - const { store, setCookieHeaders } = responseCookieStore() - writePendingAuth(store, FB_MESSENGER_PENDING_AUTH_COOKIE, "token-1") - const [setCookie] = setCookiesFor( - setCookieHeaders(), - FB_MESSENGER_PENDING_AUTH_COOKIE, - ) - return attributesOf(setCookie as string).path as string -} - -describe("RFC 6265 §5.1.4 path-match", () => { - test.each([ - ["/", "/", true], - ["/rpc", "/", true], - ["/channels/messenger/select", "/", true], - ["/channels/messenger/select", "/channels/messenger/select", true], - // The shipped bug: the connect endpoint is not under the picker's path. - ["/rpc", "/channels/messenger/select", false], - // Prefix without a `/` boundary is not a match. - ["/channels/messenger/selection", "/channels/messenger/select", false], - // Prefix with a `/` boundary is. - ["/channels/messenger/select/x", "/channels/messenger/select", true], - ["/channels", "/channels/messenger/select", false], - ])("%s vs cookie-path %s -> %s", (requestPath, cookiePath, expected) => { - expect(pathMatches(requestPath, cookiePath)).toBe(expected) - }) -}) - -describe("the pending-auth cookie's Path covers every path that reads it", () => { - test("there is still a picker route to cover", () => { - expect(pickerRoutes().length).toBeGreaterThan(0) - }) - - // Every connect now goes through the typed oRPC client, which posts to this - // one endpoint — so it is the single request path the cookie must reach. - test(`the connect endpoint ${RPC_ENDPOINT_PATH} receives the cookie`, () => { - expect(pathMatches(RPC_ENDPOINT_PATH, writtenCookiePath())).toBe(true) - }) - - test.each( - pickerRoutes(), - )("the picker page %s receives the cookie", (route) => { - expect(pathMatches(route, writtenCookiePath())).toBe(true) - }) - - test("the session-expired retry route receives the cookie", () => { - expect(pathMatches(CONNECT_RETRY_HREF, writtenCookiePath())).toBe(true) - }) -}) diff --git a/apps/builder/__tests__/facebook-pending-auth.test.ts b/apps/builder/__tests__/facebook-pending-auth.test.ts deleted file mode 100644 index a486ea640d..0000000000 --- a/apps/builder/__tests__/facebook-pending-auth.test.ts +++ /dev/null @@ -1,216 +0,0 @@ -// @vitest-environment node - -import { beforeEach, describe, expect, test, vi } from "vitest" - -// --------------------------------------------------------------------------- -// `readPendingAuth` is the ONLY source of the Messenger/Instagram connect -// actions' user token and `workspaceId` (plan §4.7) — never client input. -// It must return null for anything that isn't a genuinely fresh, unmodified, -// schema-shaped payload: a missing cookie, an expired one, a tampered -// ciphertext, or a decrypted-but-malformed JSON object (a future field -// rename/drop, or an outright forged cookie). This exercises the real -// `@chatbotx.io/encryption` primitives (ENCRYPTION_KEY is seeded by the -// vitest-config node preset's `setup-env.ts`) rather than mocking them, so -// the tamper-detection case is a genuine AES-GCM auth-tag failure. -// --------------------------------------------------------------------------- - -const mockCookies = vi.hoisted(() => vi.fn()) - -vi.mock("next/headers", () => ({ cookies: mockCookies })) - -const { - encryptAuth, - FB_INSTAGRAM_FACEBOOK_PENDING_AUTH_COOKIE, - FB_INSTAGRAM_PENDING_AUTH_COOKIE, - FB_MESSENGER_PENDING_AUTH_COOKIE, - FB_PENDING_AUTH_MAX_AGE, - readPendingAuth, - writePendingAuth, -} = await import("@/lib/facebook-pending-auth") - -function stubCookie(value: string | undefined) { - mockCookies.mockResolvedValue({ - get: (name: string) => - name === FB_MESSENGER_PENDING_AUTH_COOKIE && value !== undefined - ? { value } - : undefined, - }) -} - -const basePayload = { - userToken: "user-token-1", - workspaceId: "ws-1", - referer: "/channels/create", - version: "v23.0", - expiresAt: Date.now() + 600_000, -} - -describe("readPendingAuth", () => { - beforeEach(() => { - mockCookies.mockReset() - }) - - test("returns null when the cookie is missing", async () => { - stubCookie(undefined) - - expect(await readPendingAuth(FB_MESSENGER_PENDING_AUTH_COOKIE)).toBeNull() - }) - - test("returns the decrypted payload for a valid, unexpired, schema-shaped cookie", async () => { - const token = await encryptAuth(basePayload) - stubCookie(token) - - expect(await readPendingAuth(FB_MESSENGER_PENDING_AUTH_COOKIE)).toEqual( - basePayload, - ) - }) - - test("returns null once the payload's expiresAt has passed", async () => { - const token = await encryptAuth({ - ...basePayload, - expiresAt: Date.now() - 1000, - }) - stubCookie(token) - - expect(await readPendingAuth(FB_MESSENGER_PENDING_AUTH_COOKIE)).toBeNull() - }) - - test("returns null when the ciphertext has been tampered with", async () => { - const token = await encryptAuth(basePayload) - // Flip the envelope's base64url tail — the AES-GCM auth tag no longer - // matches, so decryption itself fails. - const tampered = token.endsWith("A") - ? `${token.slice(0, -1)}B` - : `${token.slice(0, -1)}A` - stubCookie(tampered) - - expect(await readPendingAuth(FB_MESSENGER_PENDING_AUTH_COOKIE)).toBeNull() - }) - - test("returns null for a garbage (non-envelope) cookie value", async () => { - stubCookie("not-a-valid-base64url-envelope") - - expect(await readPendingAuth(FB_MESSENGER_PENDING_AUTH_COOKIE)).toBeNull() - }) - - test("returns null when a required field is missing from the decrypted payload", async () => { - const { workspaceId: _dropped, ...invalidPayload } = basePayload - const token = await encryptAuth(invalidPayload) - stubCookie(token) - - expect(await readPendingAuth(FB_MESSENGER_PENDING_AUTH_COOKIE)).toBeNull() - }) - - test("returns null when a required field has the wrong type", async () => { - const token = await encryptAuth({ ...basePayload, expiresAt: "soon" }) - stubCookie(token) - - expect(await readPendingAuth(FB_MESSENGER_PENDING_AUTH_COOKIE)).toBeNull() - }) - - test("returns null when userToken is present but empty", async () => { - const token = await encryptAuth({ ...basePayload, userToken: "" }) - stubCookie(token) - - expect(await readPendingAuth(FB_MESSENGER_PENDING_AUTH_COOKIE)).toBeNull() - }) - - test("accepts the optional user-identity fields when present", async () => { - const withOptionalFields = { - ...basePayload, - userId: "fb-user-1", - userName: "FB User", - userAvatarUrl: "https://example.com/avatar.jpg", - } - const token = await encryptAuth(withOptionalFields) - stubCookie(token) - - expect(await readPendingAuth(FB_MESSENGER_PENDING_AUTH_COOKIE)).toEqual( - withOptionalFields, - ) - }) -}) - -/** - * The connect procedures read this cookie server-side, so its `path` has to - * cover the endpoint the typed oRPC client posts them to. It used to be - * scoped to the picker page the server action was called from — with the - * picker now posting to `/rpc`, that scoping meant the browser simply never - * sent it and every batch died on `sessionExpired`. Asserted through the - * writer, which is the only thing that reaches a real cookie store. - */ -describe("the pending-auth cookie's own options", () => { - const write = (cookieName = FB_MESSENGER_PENDING_AUTH_COOKIE) => { - const set = vi.fn() - writePendingAuth({ set }, cookieName, "token-1") - return set.mock.calls[0]?.[2] as Record - } - - test("scopes the cookie to the whole app, so the connect routes receive it", () => { - expect(write().path).toBe("/") - }) - - test("keeps every other protection the picker-scoped cookie had", () => { - expect(write()).toMatchObject({ - httpOnly: true, - sameSite: "lax", - maxAge: FB_PENDING_AUTH_MAX_AGE, - }) - }) - - test("marks the cookie secure in production only", () => { - const original = process.env.NODE_ENV - - vi.stubEnv("NODE_ENV", "production") - expect(write().secure).toBe(true) - - vi.stubEnv("NODE_ENV", "development") - expect(write().secure).toBe(false) - - vi.stubEnv("NODE_ENV", original ?? "test") - }) -}) - -/** - * A browser that started a connect just before the path widened still holds a - * picker-scoped cookie under the same name, and a path-scoped cookie wins over - * a root-scoped one on the picker page. Every write therefore expires the old - * one — otherwise the picker could keep reading a stale session for up to ten - * minutes after the deploy. - */ -describe("writePendingAuth", () => { - const setCalls = () => { - const set = vi.fn() - return { store: { set }, set } - } - - test("writes the new cookie at the root path", () => { - const { store, set } = setCalls() - - writePendingAuth(store, FB_MESSENGER_PENDING_AUTH_COOKIE, "token-1") - - expect(set).toHaveBeenNthCalledWith( - 1, - FB_MESSENGER_PENDING_AUTH_COOKIE, - "token-1", - expect.objectContaining({ path: "/", httpOnly: true, maxAge: 600 }), - ) - }) - - test.each([ - FB_MESSENGER_PENDING_AUTH_COOKIE, - FB_INSTAGRAM_PENDING_AUTH_COOKIE, - FB_INSTAGRAM_FACEBOOK_PENDING_AUTH_COOKIE, - ])("issues exactly one Set-Cookie for %s — a second set() under the same name would replace it in Next's name-keyed store", (cookieName) => { - const { store, set } = setCalls() - - writePendingAuth(store, cookieName, "token-1") - - expect(set).toHaveBeenCalledTimes(1) - expect(set).toHaveBeenCalledWith( - cookieName, - "token-1", - expect.objectContaining({ path: "/", maxAge: 600 }), - ) - }) -}) diff --git a/apps/builder/__tests__/instagram-accounts.test.tsx b/apps/builder/__tests__/instagram-accounts.test.tsx index 08369f5f6d..ad387aed3e 100644 --- a/apps/builder/__tests__/instagram-accounts.test.tsx +++ b/apps/builder/__tests__/instagram-accounts.test.tsx @@ -1,8 +1,8 @@ -import type { InstagramAccount } from "@chatbotx.io/integration-instagram" import { act } from "react" import { createRoot, type Root } from "react-dom/client" import { afterEach, beforeEach, describe, expect, test, vi } from "vitest" import { INSTAGRAM_DIRECT_CONNECT_ROUTE } from "@/features/channel-connect/lib/registry" +import type { InstagramDirectAccount } from "@/features/integration-instagram/components/instagram-accounts" /** Echoes the key back so assertions never depend on the English copy. */ vi.mock("next-intl", () => ({ @@ -67,13 +67,10 @@ const { InstagramAccounts } = await import( "@/features/integration-instagram/components/instagram-accounts" ) -const account: InstagramAccount = { - id: "page-scoped-1", +const account: InstagramDirectAccount = { + id: "ig-1", name: "IG Direct Account", - username: "ig_direct", - userId: "ig-1", - profile_picture_url: "https://example.com/avatar.jpg", - accessToken: "account-token-1", + avatarUrl: "https://example.com/avatar.jpg", } describe("InstagramAccounts", () => { @@ -97,7 +94,13 @@ describe("InstagramAccounts", () => { function renderAccount() { act(() => { - root.render() + root.render( + , + ) }) } @@ -106,15 +109,14 @@ describe("InstagramAccounts", () => { button.textContent?.includes("actions.continue"), ) - test("renders the account card with name and @username, no batch dialog ever appears", () => { + test("renders the account card with its name and no batch dialog", () => { renderAccount() expect(container.textContent).toContain("IG Direct Account") - expect(container.textContent).toContain("@ig_direct") expect(mockConnectManyDialog).not.toHaveBeenCalled() }) - test("clicking continue posts only { igId } to the instagram connect route — no token, no workspaceId, and never opens the batch dialog", async () => { + test("clicking continue posts only { sessionId, igId } to the instagram connect route — no token, no workspaceId, and never opens the batch dialog", async () => { mockConnectViaApi.mockResolvedValue({ kind: "outcome", outcome: { @@ -135,16 +137,16 @@ describe("InstagramAccounts", () => { }) expect(mockConnectViaApi).toHaveBeenCalledTimes(1) - // Ids only, and this login's own route — no token, no workspaceId. + // Session and account IDs only, and this login's own route — no token or workspaceId. expect(mockConnectViaApi.mock.calls[0]?.[0]).toMatchObject({ // The direct-login route itself — it now carries the typed oRPC // procedure, so identity is what pins this login, not a URL string. route: INSTAGRAM_DIRECT_CONNECT_ROUTE, - body: { igId: "ig-1" }, + body: { sessionId: "session-1", igId: "ig-1" }, }) expect( Object.keys(mockConnectViaApi.mock.calls[0]?.[0]?.body ?? {}), - ).toEqual(["igId"]) + ).toEqual(["sessionId", "igId"]) expect(mockConnectManyDialog).not.toHaveBeenCalled() await act(async () => { diff --git a/apps/builder/__tests__/instagram-facebook-select-page.test.tsx b/apps/builder/__tests__/instagram-facebook-select-page.test.tsx index 1b0102f16a..2ec2bfaa61 100644 --- a/apps/builder/__tests__/instagram-facebook-select-page.test.tsx +++ b/apps/builder/__tests__/instagram-facebook-select-page.test.tsx @@ -4,31 +4,31 @@ import { renderToStaticMarkup } from "react-dom/server" import { beforeEach, describe, expect, test, vi } from "vitest" type SelectFacebookAccountsElementProps = { - workspaceId: string items: Array<{ + disabled?: boolean + disabledReason?: string id: string name: string secondary?: string - disabled?: boolean - disabledReason?: string }> + sessionId: string + workspaceId: string } const { - mockFindConnectedIgIds, - mockGetUserInstagramAccounts, - mockReadPendingAuth, + mockGetCurrentUserId, mockRedirect, + mockResolveConnectSession, mockSelectFacebookAccounts, } = vi.hoisted(() => ({ - mockFindConnectedIgIds: vi.fn(), - mockGetUserInstagramAccounts: vi.fn(), - mockReadPendingAuth: vi.fn(), + mockGetCurrentUserId: vi.fn(), mockRedirect: vi.fn((path: string) => { throw new Error(`redirect:${path}`) }), + mockResolveConnectSession: vi.fn(), mockSelectFacebookAccounts: vi.fn( - (_props: { workspaceId: string; items: unknown[] }) => null, + (_props: { items: unknown[]; sessionId: string; workspaceId: string }) => + null, ), })) @@ -41,18 +41,12 @@ vi.mock("next-intl/server", () => ({ getTranslations: async () => (key: string) => key, })) -vi.mock("@chatbotx.io/business", () => ({ - instagramIntegrationService: { findConnectedIgIds: mockFindConnectedIgIds }, -})) - -vi.mock("@chatbotx.io/integration-instagram-facebook", () => ({ - getUserInstagramAccounts: mockGetUserInstagramAccounts, +vi.mock("@/lib/auth/utils", () => ({ + getCurrentUserId: mockGetCurrentUserId, })) -vi.mock("@/lib/facebook-pending-auth", () => ({ - readPendingAuth: mockReadPendingAuth, - FB_INSTAGRAM_FACEBOOK_PENDING_AUTH_COOKIE: - "fb_instagram_facebook_pending_auth", +vi.mock("@/features/channel-connect/lib/resolve-connect-session", () => ({ + resolveConnectSession: mockResolveConnectSession, })) vi.mock("@/features/inboxes/components/inbox-icon", () => ({ @@ -70,85 +64,96 @@ const { default: InstagramFacebookSelectPage } = await import( "../src/app/(no-sidebar)/channels/instagram-facebook/select/page" ) -const connectableAccount = { +const pageArgs = { + searchParams: Promise.resolve({ session: "session-1" }), +} + +const connectableTarget = { + avatarUrl: "https://example.com/connectable.jpg", id: "ig-connectable", name: "Connectable Account", - username: "connectable", - profile_picture_url: "https://example.com/connectable.jpg", - pageId: "page-connectable", - pageAccessToken: "connectable-token", + selectable: true, } -const connectedAccount = { +const connectedTarget = { id: "ig-connected", name: "Connected Account", - username: "connected", - pageId: "page-connected", - pageAccessToken: "connected-token", + alreadyConnected: "other_workspace" as const, + selectable: false, } describe("InstagramFacebookSelectPage", () => { beforeEach(() => { vi.clearAllMocks() - mockReadPendingAuth.mockResolvedValue({ - userToken: "user-token", - version: "v23.0", - referer: "/channels/create", - workspaceId: "ws-1", - expiresAt: Date.now() + 600_000, + mockGetCurrentUserId.mockResolvedValue("user-1") + mockResolveConnectSession.mockResolvedValue({ + session: { + targets: [connectedTarget, connectableTarget], + }, + workspace: { id: "ws-1" }, }) - mockGetUserInstagramAccounts.mockResolvedValue([ - connectedAccount, - connectableAccount, - ]) - mockFindConnectedIgIds.mockResolvedValue(new Set(["ig-connected"])) }) - test("passes every account through as a picker item, ranked selectable first then already-connected, and never sends the page access token", async () => { - const element = await InstagramFacebookSelectPage() + test("passes every session target through as a picker item, ranked selectable first then already-connected", async () => { + const element = await InstagramFacebookSelectPage(pageArgs) renderToStaticMarkup(element) + expect(mockResolveConnectSession).toHaveBeenCalledWith({ + userId: "user-1", + sessionId: "session-1", + credentialType: "instagramFacebook", + brandingChannel: "instagram", + }) expect(mockSelectFacebookAccounts).toHaveBeenCalledTimes(1) const props = mockSelectFacebookAccounts.mock.calls[0]?.[0] as | SelectFacebookAccountsElementProps | undefined + expect(props?.sessionId).toBe("session-1") expect(props?.workspaceId).toBe("ws-1") expect(props?.items).toEqual([ expect.objectContaining({ id: "ig-connectable", name: "Connectable Account", - secondary: "@connectable", + secondary: "ig-connectable", disabled: false, }), expect.objectContaining({ id: "ig-connected", name: "Connected Account", - secondary: "@connected", + secondary: "ig-connected", disabled: true, disabledReason: "instagram.selectPage.alreadyConnectedNote", }), ]) + }) + + test("redirects to channel creation when the session id is missing", async () => { + await expect( + InstagramFacebookSelectPage({ searchParams: Promise.resolve({}) }), + ).rejects.toThrow("redirect:/channels/create") - for (const item of props?.items ?? []) { - expect(item).not.toHaveProperty("pageAccessToken") - } + expect(mockGetCurrentUserId).not.toHaveBeenCalled() + expect(mockResolveConnectSession).not.toHaveBeenCalled() }) - test("redirects to channel creation when the pending-auth cookie is missing or invalid", async () => { - mockReadPendingAuth.mockResolvedValue(null) + test("redirects to channel creation when the user is not authenticated", async () => { + mockGetCurrentUserId.mockResolvedValue(null) - await expect(InstagramFacebookSelectPage()).rejects.toThrow( + await expect(InstagramFacebookSelectPage(pageArgs)).rejects.toThrow( "redirect:/channels/create", ) - expect(mockGetUserInstagramAccounts).not.toHaveBeenCalled() + + expect(mockResolveConnectSession).not.toHaveBeenCalled() }) - test("renders with zero items (no redirect) when the user has no Instagram business accounts — the picker shows its own empty state", async () => { - mockGetUserInstagramAccounts.mockResolvedValue([]) - mockFindConnectedIgIds.mockResolvedValue(new Set()) + test("renders with zero items when the session has no Instagram business accounts", async () => { + mockResolveConnectSession.mockResolvedValue({ + session: { targets: [] }, + workspace: { id: "ws-1" }, + }) - const element = await InstagramFacebookSelectPage() + const element = await InstagramFacebookSelectPage(pageArgs) renderToStaticMarkup(element) expect(mockRedirect).not.toHaveBeenCalled() diff --git a/apps/builder/__tests__/instagram-select-account-action.test.ts b/apps/builder/__tests__/instagram-select-account-action.test.ts index 197db079b6..6f54fd8e29 100644 --- a/apps/builder/__tests__/instagram-select-account-action.test.ts +++ b/apps/builder/__tests__/instagram-select-account-action.test.ts @@ -6,68 +6,40 @@ import { credentialMissingException, notWorkspaceMemberException, } from "@chatbotx.io/business/errors" -import { SdkException } from "@chatbotx.io/sdk" import { beforeEach, describe, expect, test, vi } from "vitest" -// --------------------------------------------------------------------------- -// `connectInstagramAccount` (Instagram direct login, plan §3.3) never trusts the -// wire payload for anything but the `igId` cross-check — the account is -// re-resolved server-side from `getInstagramAccount(cookie.userToken)` on -// every request. Session-level steps 1-4 are delegated to -// `resolveConnectSession` (mocked here as one function, exactly like the -// Messenger/Instagram-via-Facebook action tests). `findConnectedIgIds` is -// checked BEFORE the webhook subscribe, unlike the pre-phase-4 code. -// --------------------------------------------------------------------------- - const { buildContextMock, - connectAccountMock, - findConnectedIgIdsMock, - getInstagramAccountMock, - loggerWarnMock, + connectTargetsMock, + findByInboxIdMock, loggerErrorMock, - persistIntegrationUserInfoMock, + loggerWarnMock, resolveConnectSessionMock, runChannelHandlerMock, - subscribePageToInstagramWebhookMock, - updateUserInfoMock, updateWorkspaceLogoMock, } = vi.hoisted(() => ({ buildContextMock: vi.fn(), - connectAccountMock: vi.fn(), - findConnectedIgIdsMock: vi.fn(), - getInstagramAccountMock: vi.fn(), - loggerWarnMock: vi.fn(), + connectTargetsMock: vi.fn(), + findByInboxIdMock: vi.fn(), loggerErrorMock: vi.fn(), - persistIntegrationUserInfoMock: vi.fn(), + loggerWarnMock: vi.fn(), resolveConnectSessionMock: vi.fn(), runChannelHandlerMock: vi.fn(), - subscribePageToInstagramWebhookMock: vi.fn(), - updateUserInfoMock: vi.fn(), updateWorkspaceLogoMock: vi.fn(), })) -vi.mock("@/lib/facebook-pending-auth", () => ({ - FB_INSTAGRAM_PENDING_AUTH_COOKIE: "fb_instagram_pending_auth", -})) - vi.mock("@/features/channel-connect/lib/resolve-connect-session", () => ({ resolveConnectSession: resolveConnectSessionMock, })) vi.mock("@/features/integration-webchat/lib", () => ({ BRANDING_TITLE: "ChatbotX", - getBrandingUrl: () => "https://app.test/branding", })) vi.mock("@/features/workspaces/actions/upload-logo", () => ({ updateWorkspaceLogo: updateWorkspaceLogoMock, })) -vi.mock("@/lib/integration-user-info", () => ({ - persistIntegrationUserInfo: persistIntegrationUserInfoMock, -})) - vi.mock("@/lib/log", () => ({ logger: { warn: loggerWarnMock, error: loggerErrorMock, info: vi.fn() }, })) @@ -75,21 +47,16 @@ vi.mock("@/lib/log", () => ({ vi.mock("@chatbotx.io/business", () => ({ buildContext: buildContextMock, instagramIntegrationService: { - findConnectedIgIds: findConnectedIgIdsMock, - connectAccount: connectAccountMock, - updateUserInfo: updateUserInfoMock, + findByInboxId: findByInboxIdMock, }, })) -vi.mock("@chatbotx.io/integration-instagram", () => ({ - getInstagramAccount: getInstagramAccountMock, - integration: { runChannelHandler: runChannelHandlerMock }, - subscribePageToInstagramWebhook: subscribePageToInstagramWebhookMock, +vi.mock("@chatbotx.io/connections", () => ({ + connectionService: { connectTargets: connectTargetsMock }, })) -vi.mock("@chatbotx.io/sdk", () => ({ - AuthType: { oauth2: "oauth2" }, - SdkException: class SdkException extends Error {}, +vi.mock("@chatbotx.io/integration-instagram", () => ({ + integration: { runChannelHandler: runChannelHandlerMock }, })) const { connectInstagramAccount } = await import( @@ -98,33 +65,18 @@ const { connectInstagramAccount } = await import( const call = connectInstagramAccount -const resolvedAccount = { - id: "page-scoped-1", - name: "IG Direct Account", - username: "ig_direct", - userId: "ig1", - profile_picture_url: "https://example.com/avatar.jpg", - accessToken: "account-token-1", -} - const resolvedSession = { - pendingAuth: { - userToken: "user-token-1", - workspaceId: "ws-1", - referer: "/channels/create", - version: "v23.0", - expiresAt: Date.now() + 600_000, + session: { + targets: [ + { + id: "ig1", + name: "IG Direct Account", + selectable: true, + }, + ], }, workspace: { id: "ws-1", ownerId: "owner-1" }, platformOwnerId: "owner-1", - credential: { - config: { - clientId: "client-1", - clientSecret: "secret-1", - version: "v23.0", - }, - }, - appUrl: "https://app.test", brandingMenuEntry: { label: "ChatbotX", type: "url" as const, @@ -137,22 +89,20 @@ describe("connectInstagramAccount (Instagram direct login)", () => { vi.clearAllMocks() resolveConnectSessionMock.mockResolvedValue(resolvedSession) - getInstagramAccountMock.mockResolvedValue(resolvedAccount) - findConnectedIgIdsMock.mockResolvedValue(new Set()) - subscribePageToInstagramWebhookMock.mockResolvedValue(undefined) - connectAccountMock.mockResolvedValue({ - workspaceId: "ws-1", - integrationId: "integration-1", - wasCreated: true, - integration: { id: "integration-1", workspaceId: "ws-1" }, + connectTargetsMock.mockResolvedValue({ + outcomes: [{ sourceId: "ig1", status: "connected" }], + connections: [{ inboxId: "inbox-1" }], + }) + findByInboxIdMock.mockResolvedValue({ + id: "integration-1", + auth: {}, }) runChannelHandlerMock.mockResolvedValue(undefined) updateWorkspaceLogoMock.mockResolvedValue(undefined) - persistIntegrationUserInfoMock.mockResolvedValue(undefined) buildContextMock.mockResolvedValue({}) }) - test("returns sessionExpired and touches nothing else when the pending-auth cookie is missing/invalid", async () => { + test("returns sessionExpired and touches nothing else when the connect session is missing/invalid", async () => { resolveConnectSessionMock.mockRejectedValue( new ChatbotXException( "Your connect session expired. Please start again.", @@ -162,23 +112,25 @@ describe("connectInstagramAccount (Instagram direct login)", () => { const result = await call({ userId: "user-1", + sessionId: "session-1", igId: "ig1", }) expect(result).toEqual({ kind: "sessionError", code: "sessionExpired" }) - expect(getInstagramAccountMock).not.toHaveBeenCalled() + expect(connectTargetsMock).not.toHaveBeenCalled() }) - test("returns notMember before any provider call when the resolver rejects membership (owner gate)", async () => { + test("returns notMember before connecting when the resolver rejects membership", async () => { resolveConnectSessionMock.mockRejectedValue(notWorkspaceMemberException()) const result = await call({ userId: "user-1", + sessionId: "session-1", igId: "ig1", }) expect(result).toEqual({ kind: "sessionError", code: "notMember" }) - expect(getInstagramAccountMock).not.toHaveBeenCalled() + expect(connectTargetsMock).not.toHaveBeenCalled() }) test("returns trialExpired when the workspace owner is blocked", async () => { @@ -188,11 +140,12 @@ describe("connectInstagramAccount (Instagram direct login)", () => { const result = await call({ userId: "user-1", + sessionId: "session-1", igId: "ig1", }) expect(result).toEqual({ kind: "sessionError", code: "trialExpired" }) - expect(getInstagramAccountMock).not.toHaveBeenCalled() + expect(connectTargetsMock).not.toHaveBeenCalled() }) test("returns credentialMissing when the workspace has no Instagram app credential", async () => { @@ -204,43 +157,45 @@ describe("connectInstagramAccount (Instagram direct login)", () => { const result = await call({ userId: "user-1", + sessionId: "session-1", igId: "ig1", }) expect(result).toEqual({ kind: "sessionError", code: "credentialMissing" }) - expect(getInstagramAccountMock).not.toHaveBeenCalled() + expect(connectTargetsMock).not.toHaveBeenCalled() }) - test("an igId that doesn't match the re-resolved account resolves to notSelectable without persisting", async () => { - getInstagramAccountMock.mockResolvedValue({ - ...resolvedAccount, - userId: "some-other-ig-id", - }) - + test("an unknown igId resolves to notSelectable without connecting", async () => { const result = await call({ userId: "user-1", - igId: "ig1", + sessionId: "session-1", + igId: "forged-id", }) expect(result).toEqual({ kind: "outcome", outcome: { - sourceId: "ig1", - name: "ig1", + sourceId: "forged-id", + name: "forged-id", status: "failed", reason: "notSelectable", coexistEligible: false, }, }) - expect(subscribePageToInstagramWebhookMock).not.toHaveBeenCalled() - expect(connectAccountMock).not.toHaveBeenCalled() + expect(connectTargetsMock).not.toHaveBeenCalled() }) - test("a null re-resolved account (unsupported account type) resolves to notSelectable", async () => { - getInstagramAccountMock.mockResolvedValue(null) + test("a non-selectable account resolves to notSelectable without connecting", async () => { + resolveConnectSessionMock.mockResolvedValue({ + ...resolvedSession, + session: { + targets: [{ id: "ig1", name: "IG Direct Account", selectable: false }], + }, + }) const result = await call({ userId: "user-1", + sessionId: "session-1", igId: "ig1", }) @@ -248,20 +203,33 @@ describe("connectInstagramAccount (Instagram direct login)", () => { kind: "outcome", outcome: { sourceId: "ig1", - name: "ig1", + name: "IG Direct Account", status: "failed", reason: "notSelectable", coexistEligible: false, }, }) - expect(connectAccountMock).not.toHaveBeenCalled() + expect(connectTargetsMock).not.toHaveBeenCalled() }) - test("an already-connected account resolves to duplicated with NO subscribe call", async () => { - findConnectedIgIdsMock.mockResolvedValue(new Set(["ig1"])) + test("an already-connected account resolves to duplicated without connecting", async () => { + resolveConnectSessionMock.mockResolvedValue({ + ...resolvedSession, + session: { + targets: [ + { + id: "ig1", + name: "IG Direct Account", + selectable: false, + alreadyConnected: "this_workspace", + }, + ], + }, + }) const result = await call({ userId: "user-1", + sessionId: "session-1", igId: "ig1", }) @@ -275,13 +243,13 @@ describe("connectInstagramAccount (Instagram direct login)", () => { coexistEligible: false, }, }) - expect(subscribePageToInstagramWebhookMock).not.toHaveBeenCalled() - expect(connectAccountMock).not.toHaveBeenCalled() + expect(connectTargetsMock).not.toHaveBeenCalled() }) - test("happy path: getInstagramAccount -> findConnectedIgIds -> subscribe -> persist -> follow-ups, in order", async () => { + test("connects the selected target and runs Instagram follow-ups", async () => { const result = await call({ userId: "user-1", + sessionId: "session-1", igId: "ig1", }) @@ -296,48 +264,29 @@ describe("connectInstagramAccount (Instagram direct login)", () => { coexistEligible: true, }, }) - - expect(getInstagramAccountMock).toHaveBeenCalledTimes(1) - expect(subscribePageToInstagramWebhookMock).toHaveBeenCalledWith( - expect.objectContaining({ - igId: "page-scoped-1", - accessToken: "account-token-1", - }), - ) - expect(connectAccountMock).toHaveBeenCalledWith( - expect.objectContaining({ - actorUserId: "user-1", - ownerId: "owner-1", - workspaceId: "ws-1", - type: "instagram", - account: { - igId: "ig1", - igName: "IG Direct Account", - igUsername: "ig_direct", - pageId: "page-scoped-1", - }, - }), - ) - - const order = [ - getInstagramAccountMock, - findConnectedIgIdsMock, - subscribePageToInstagramWebhookMock, - connectAccountMock, - runChannelHandlerMock, - updateWorkspaceLogoMock, - persistIntegrationUserInfoMock, - ].map((mock) => mock.mock.invocationCallOrder[0]) - expect(order).toEqual([...order].sort((a, b) => a - b)) + expect(connectTargetsMock).toHaveBeenCalledWith({ + sessionId: "session-1", + workspaceId: "ws-1", + targetIds: ["ig1"], + actorUserId: "user-1", + }) }) - test("a subscribe-webhook failure resolves to providerRejected and never persists", async () => { - subscribePageToInstagramWebhookMock.mockRejectedValue( - new SdkException("Meta rejected the subscription"), - ) + test("a failed connection outcome is returned without follow-ups", async () => { + connectTargetsMock.mockResolvedValue({ + outcomes: [ + { + sourceId: "ig1", + status: "failed", + reason: "providerRejected", + }, + ], + connections: [], + }) const result = await call({ userId: "user-1", + sessionId: "session-1", igId: "ig1", }) @@ -351,7 +300,7 @@ describe("connectInstagramAccount (Instagram direct login)", () => { coexistEligible: false, }, }) - expect(connectAccountMock).not.toHaveBeenCalled() + expect(runChannelHandlerMock).not.toHaveBeenCalled() }) test("a follow-up failure still returns a connected outcome, carrying a followUpFailed warning", async () => { @@ -359,6 +308,7 @@ describe("connectInstagramAccount (Instagram direct login)", () => { const result = await call({ userId: "user-1", + sessionId: "session-1", igId: "ig1", }) @@ -376,11 +326,12 @@ describe("connectInstagramAccount (Instagram direct login)", () => { expect(loggerWarnMock).toHaveBeenCalled() }) - test("a unique-violation race during persist resolves to duplicated", async () => { - connectAccountMock.mockRejectedValue(channelDuplicatedException()) + test("a unique-violation race during connection resolves to duplicated", async () => { + connectTargetsMock.mockRejectedValue(channelDuplicatedException()) const result = await call({ userId: "user-1", + sessionId: "session-1", igId: "ig1", }) diff --git a/apps/builder/__tests__/instagram-select-account-facebook-action.test.ts b/apps/builder/__tests__/instagram-select-account-facebook-action.test.ts index f099ab75d5..233125c82e 100644 --- a/apps/builder/__tests__/instagram-select-account-facebook-action.test.ts +++ b/apps/builder/__tests__/instagram-select-account-facebook-action.test.ts @@ -6,71 +6,40 @@ import { credentialMissingException, notWorkspaceMemberException, } from "@chatbotx.io/business/errors" -import { SdkException } from "@chatbotx.io/sdk" import { beforeEach, describe, expect, test, vi } from "vitest" -// --------------------------------------------------------------------------- -// `connectInstagramAccountViaFacebook` follows the shared per-account connect -// skeleton (plan §2.4), mirroring Messenger's `selectPageAction`: plan §2.4 -// steps 1-4 (pending-auth cookie -> workspace/membership -> owner gate -> -// credential + branding) are delegated to the shared `resolveConnectSession` -// helper (mocked here as one function so this file only asserts the -// ACTION's own behavior). `getUserInstagramAccounts` runs once per request -// (no cache — provider lists carry page access tokens, §4.9), and the -// webhook subscribe now runs BEFORE the persist call (plan §3.2 — it used to -// run after commit). -// --------------------------------------------------------------------------- - const { buildContextMock, - connectAccountMock, - findConnectedIgIdsMock, - getUserInstagramAccountsMock, - loggerWarnMock, + connectTargetsMock, + findByInboxIdMock, loggerErrorMock, - persistIntegrationUserInfoMock, + loggerWarnMock, resolveConnectSessionMock, runChannelHandlerMock, - subscribePageToInstagramWebhookMock, - updateUserInfoMock, updateWorkspaceLogoMock, } = vi.hoisted(() => ({ buildContextMock: vi.fn(), - connectAccountMock: vi.fn(), - findConnectedIgIdsMock: vi.fn(), - getUserInstagramAccountsMock: vi.fn(), - loggerWarnMock: vi.fn(), + connectTargetsMock: vi.fn(), + findByInboxIdMock: vi.fn(), loggerErrorMock: vi.fn(), - persistIntegrationUserInfoMock: vi.fn(), + loggerWarnMock: vi.fn(), resolveConnectSessionMock: vi.fn(), runChannelHandlerMock: vi.fn(), - subscribePageToInstagramWebhookMock: vi.fn(), - updateUserInfoMock: vi.fn(), updateWorkspaceLogoMock: vi.fn(), })) -vi.mock("@/lib/facebook-pending-auth", () => ({ - FB_INSTAGRAM_FACEBOOK_PENDING_AUTH_COOKIE: - "fb_instagram_facebook_pending_auth", -})) - vi.mock("@/features/channel-connect/lib/resolve-connect-session", () => ({ resolveConnectSession: resolveConnectSessionMock, })) vi.mock("@/features/integration-webchat/lib", () => ({ BRANDING_TITLE: "ChatbotX", - getBrandingUrl: () => "https://app.test/branding", })) vi.mock("@/features/workspaces/actions/upload-logo", () => ({ updateWorkspaceLogo: updateWorkspaceLogoMock, })) -vi.mock("@/lib/integration-user-info", () => ({ - persistIntegrationUserInfo: persistIntegrationUserInfoMock, -})) - vi.mock("@/lib/log", () => ({ logger: { warn: loggerWarnMock, error: loggerErrorMock, info: vi.fn() }, })) @@ -78,21 +47,16 @@ vi.mock("@/lib/log", () => ({ vi.mock("@chatbotx.io/business", () => ({ buildContext: buildContextMock, instagramIntegrationService: { - findConnectedIgIds: findConnectedIgIdsMock, - connectAccount: connectAccountMock, - updateUserInfo: updateUserInfoMock, + findByInboxId: findByInboxIdMock, }, })) -vi.mock("@chatbotx.io/integration-instagram-facebook", () => ({ - getUserInstagramAccounts: getUserInstagramAccountsMock, - integration: { runChannelHandler: runChannelHandlerMock }, - subscribePageToInstagramWebhook: subscribePageToInstagramWebhookMock, +vi.mock("@chatbotx.io/connections", () => ({ + connectionService: { connectTargets: connectTargetsMock }, })) -vi.mock("@chatbotx.io/sdk", () => ({ - AuthType: { oauth2: "oauth2" }, - SdkException: class SdkException extends Error {}, +vi.mock("@chatbotx.io/integration-instagram-facebook", () => ({ + integration: { runChannelHandler: runChannelHandlerMock }, })) const { connectInstagramAccountViaFacebook } = await import( @@ -101,36 +65,18 @@ const { connectInstagramAccountViaFacebook } = await import( const call = connectInstagramAccountViaFacebook -const connectableAccount = { - id: "ig1", - name: "IG Account", - username: "ig_account", - profile_picture_url: "https://example.com/avatar.jpg", - pageId: "page-1", - pageAccessToken: "page-token-1", -} - const resolvedSession = { - pendingAuth: { - userToken: "user-token-1", - userId: "fb-user-1", - userName: "FB User", - userAvatarUrl: "https://example.com/avatar.jpg", - workspaceId: "ws-1", - referer: "/channels/create", - version: "v23.0", - expiresAt: Date.now() + 600_000, + session: { + targets: [ + { + id: "ig1", + name: "IG Account", + selectable: true, + }, + ], }, workspace: { id: "ws-1", ownerId: "owner-1" }, platformOwnerId: "owner-1", - credential: { - config: { - clientId: "client-1", - clientSecret: "secret-1", - version: "v23.0", - }, - }, - appUrl: "https://app.test", brandingMenuEntry: { label: "ChatbotX", type: "url" as const, @@ -143,22 +89,20 @@ describe("connectInstagramAccountViaFacebook", () => { vi.clearAllMocks() resolveConnectSessionMock.mockResolvedValue(resolvedSession) - getUserInstagramAccountsMock.mockResolvedValue([connectableAccount]) - findConnectedIgIdsMock.mockResolvedValue(new Set()) - subscribePageToInstagramWebhookMock.mockResolvedValue(undefined) - connectAccountMock.mockResolvedValue({ - workspaceId: "ws-1", - integrationId: "integration-1", - wasCreated: true, - integration: { id: "integration-1", workspaceId: "ws-1" }, + connectTargetsMock.mockResolvedValue({ + outcomes: [{ sourceId: "ig1", status: "connected" }], + connections: [{ inboxId: "inbox-1" }], + }) + findByInboxIdMock.mockResolvedValue({ + id: "integration-1", + auth: {}, }) runChannelHandlerMock.mockResolvedValue(undefined) updateWorkspaceLogoMock.mockResolvedValue(undefined) - persistIntegrationUserInfoMock.mockResolvedValue(undefined) buildContextMock.mockResolvedValue({}) }) - test("returns sessionExpired and touches nothing else when the pending-auth cookie is missing/invalid", async () => { + test("returns sessionExpired and touches nothing else when the connect session is missing/invalid", async () => { resolveConnectSessionMock.mockRejectedValue( new ChatbotXException( "Your connect session expired. Please start again.", @@ -168,23 +112,25 @@ describe("connectInstagramAccountViaFacebook", () => { const result = await call({ userId: "user-1", + sessionId: "session-1", igId: "ig1", }) expect(result).toEqual({ kind: "sessionError", code: "sessionExpired" }) - expect(getUserInstagramAccountsMock).not.toHaveBeenCalled() + expect(connectTargetsMock).not.toHaveBeenCalled() }) - test("returns notMember before any provider call when the resolver rejects membership", async () => { + test("returns notMember before connecting when the resolver rejects membership", async () => { resolveConnectSessionMock.mockRejectedValue(notWorkspaceMemberException()) const result = await call({ userId: "user-1", + sessionId: "session-1", igId: "ig1", }) expect(result).toEqual({ kind: "sessionError", code: "notMember" }) - expect(getUserInstagramAccountsMock).not.toHaveBeenCalled() + expect(connectTargetsMock).not.toHaveBeenCalled() }) test("returns trialExpired when the workspace owner is blocked", async () => { @@ -194,11 +140,12 @@ describe("connectInstagramAccountViaFacebook", () => { const result = await call({ userId: "user-1", + sessionId: "session-1", igId: "ig1", }) expect(result).toEqual({ kind: "sessionError", code: "trialExpired" }) - expect(getUserInstagramAccountsMock).not.toHaveBeenCalled() + expect(connectTargetsMock).not.toHaveBeenCalled() }) test("returns macLimitReached when the workspace owner is blocked on MAC", async () => { @@ -212,6 +159,7 @@ describe("connectInstagramAccountViaFacebook", () => { const result = await call({ userId: "user-1", + sessionId: "session-1", igId: "ig1", }) @@ -227,16 +175,18 @@ describe("connectInstagramAccountViaFacebook", () => { const result = await call({ userId: "user-1", + sessionId: "session-1", igId: "ig1", }) expect(result).toEqual({ kind: "sessionError", code: "credentialMissing" }) - expect(getUserInstagramAccountsMock).not.toHaveBeenCalled() + expect(connectTargetsMock).not.toHaveBeenCalled() }) - test("a forged/unknown account id resolves to notSelectable without any Meta call", async () => { + test("a forged/unknown account id resolves to notSelectable without connecting", async () => { const result = await call({ userId: "user-1", + sessionId: "session-1", igId: "forged-id", }) @@ -250,15 +200,54 @@ describe("connectInstagramAccountViaFacebook", () => { coexistEligible: false, }, }) - expect(subscribePageToInstagramWebhookMock).not.toHaveBeenCalled() - expect(connectAccountMock).not.toHaveBeenCalled() + expect(connectTargetsMock).not.toHaveBeenCalled() + }) + + test("a non-selectable account resolves to notSelectable without connecting", async () => { + resolveConnectSessionMock.mockResolvedValue({ + ...resolvedSession, + session: { + targets: [{ id: "ig1", name: "IG Account", selectable: false }], + }, + }) + + const result = await call({ + userId: "user-1", + sessionId: "session-1", + igId: "ig1", + }) + + expect(result).toEqual({ + kind: "outcome", + outcome: { + sourceId: "ig1", + name: "IG Account", + status: "failed", + reason: "notSelectable", + coexistEligible: false, + }, + }) + expect(connectTargetsMock).not.toHaveBeenCalled() }) - test("an already-connected account resolves to duplicated without any further Meta call", async () => { - findConnectedIgIdsMock.mockResolvedValue(new Set(["ig1"])) + test("an already-connected account resolves to duplicated without connecting", async () => { + resolveConnectSessionMock.mockResolvedValue({ + ...resolvedSession, + session: { + targets: [ + { + id: "ig1", + name: "IG Account", + selectable: false, + alreadyConnected: "this_workspace", + }, + ], + }, + }) const result = await call({ userId: "user-1", + sessionId: "session-1", igId: "ig1", }) @@ -272,13 +261,13 @@ describe("connectInstagramAccountViaFacebook", () => { coexistEligible: false, }, }) - expect(subscribePageToInstagramWebhookMock).not.toHaveBeenCalled() - expect(connectAccountMock).not.toHaveBeenCalled() + expect(connectTargetsMock).not.toHaveBeenCalled() }) - test("happy path: getUserInstagramAccounts -> subscribe -> persist -> follow-ups, one Graph list call per request", async () => { + test("connects the selected target and runs Instagram follow-ups", async () => { const result = await call({ userId: "user-1", + sessionId: "session-1", igId: "ig1", }) @@ -293,47 +282,29 @@ describe("connectInstagramAccountViaFacebook", () => { coexistEligible: true, }, }) - - expect(getUserInstagramAccountsMock).toHaveBeenCalledTimes(1) - expect(subscribePageToInstagramWebhookMock).toHaveBeenCalledWith( - expect.objectContaining({ - pageId: "page-1", - accessToken: "page-token-1", - }), - ) - expect(connectAccountMock).toHaveBeenCalledWith( - expect.objectContaining({ - actorUserId: "user-1", - ownerId: "owner-1", - workspaceId: "ws-1", - type: "facebook", - account: { - igId: "ig1", - igName: "IG Account", - igUsername: "ig_account", - pageId: "page-1", - }, - }), - ) - - const order = [ - getUserInstagramAccountsMock, - subscribePageToInstagramWebhookMock, - connectAccountMock, - runChannelHandlerMock, - updateWorkspaceLogoMock, - persistIntegrationUserInfoMock, - ].map((mock) => mock.mock.invocationCallOrder[0]) - expect(order).toEqual([...order].sort((a, b) => a - b)) + expect(connectTargetsMock).toHaveBeenCalledWith({ + sessionId: "session-1", + workspaceId: "ws-1", + targetIds: ["ig1"], + actorUserId: "user-1", + }) }) - test("a subscribe-webhook failure resolves to providerRejected and never persists", async () => { - subscribePageToInstagramWebhookMock.mockRejectedValue( - new SdkException("Meta rejected the subscription"), - ) + test("a failed connection outcome is returned without follow-ups", async () => { + connectTargetsMock.mockResolvedValue({ + outcomes: [ + { + sourceId: "ig1", + status: "failed", + reason: "providerRejected", + }, + ], + connections: [], + }) const result = await call({ userId: "user-1", + sessionId: "session-1", igId: "ig1", }) @@ -347,7 +318,7 @@ describe("connectInstagramAccountViaFacebook", () => { coexistEligible: false, }, }) - expect(connectAccountMock).not.toHaveBeenCalled() + expect(runChannelHandlerMock).not.toHaveBeenCalled() }) test("a follow-up failure still returns a connected outcome, carrying a followUpFailed warning", async () => { @@ -355,6 +326,7 @@ describe("connectInstagramAccountViaFacebook", () => { const result = await call({ userId: "user-1", + sessionId: "session-1", igId: "ig1", }) @@ -372,11 +344,12 @@ describe("connectInstagramAccountViaFacebook", () => { expect(loggerWarnMock).toHaveBeenCalled() }) - test("a unique-violation race during persist resolves to duplicated", async () => { - connectAccountMock.mockRejectedValue(channelDuplicatedException()) + test("a unique-violation race during connection resolves to duplicated", async () => { + connectTargetsMock.mockRejectedValue(channelDuplicatedException()) const result = await call({ userId: "user-1", + sessionId: "session-1", igId: "ig1", }) diff --git a/apps/builder/__tests__/instagram-select-page.test.tsx b/apps/builder/__tests__/instagram-select-page.test.tsx index 71cac4d58c..b687994032 100644 --- a/apps/builder/__tests__/instagram-select-page.test.tsx +++ b/apps/builder/__tests__/instagram-select-page.test.tsx @@ -4,16 +4,16 @@ import { isValidElement } from "react" import { beforeEach, describe, expect, test, vi } from "vitest" const { - mockGetInstagramAccount, - mockReadPendingAuth, + mockGetCurrentUserId, mockRedirect, + mockResolveConnectSession, mockSelectAccount, } = vi.hoisted(() => ({ - mockGetInstagramAccount: vi.fn(), - mockReadPendingAuth: vi.fn(), + mockGetCurrentUserId: vi.fn(), mockRedirect: vi.fn((path: string) => { throw new Error(`redirect:${path}`) }), + mockResolveConnectSession: vi.fn(), mockSelectAccount: vi.fn(() => null), })) @@ -21,13 +21,12 @@ vi.mock("next/navigation", () => ({ redirect: mockRedirect, })) -vi.mock("@chatbotx.io/integration-instagram", () => ({ - getInstagramAccount: mockGetInstagramAccount, +vi.mock("@/lib/auth/utils", () => ({ + getCurrentUserId: mockGetCurrentUserId, })) -vi.mock("@/lib/facebook-pending-auth", () => ({ - readPendingAuth: mockReadPendingAuth, - FB_INSTAGRAM_PENDING_AUTH_COOKIE: "fb_instagram_pending_auth", +vi.mock("@/features/channel-connect/lib/resolve-connect-session", () => ({ + resolveConnectSession: mockResolveConnectSession, })) vi.mock("@/features/integration-instagram/components/select-accounts", () => ({ @@ -39,50 +38,81 @@ const { default: InstagramSelectPage } = await import( ) type SelectAccountElementProps = { - account: { id: string; username: string } + account: { avatarUrl?: string; id: string; name: string } + sessionId: string workspaceId: string } -const account = { id: "ig-1", username: "handle" } +const pageArgs = { + searchParams: Promise.resolve({ session: "session-1" }), +} + +const account = { + avatarUrl: "https://example.com/account.jpg", + id: "ig-1", + name: "Instagram account", + selectable: true, +} describe("InstagramSelectPage", () => { beforeEach(() => { vi.clearAllMocks() - mockReadPendingAuth.mockResolvedValue({ - userToken: "user-token", - version: "v23.0", - referer: "/channels/create", - workspaceId: "ws-1", - expiresAt: Date.now() + 600_000, + mockGetCurrentUserId.mockResolvedValue("user-1") + mockResolveConnectSession.mockResolvedValue({ + session: { targets: [account] }, + workspace: { id: "ws-1" }, }) - mockGetInstagramAccount.mockResolvedValue(account) }) - test("renders SelectAccount with the resolved account and workspace id", async () => { - const element = await InstagramSelectPage() + test("renders SelectAccount with the resolved account, session id, and workspace id", async () => { + const element = await InstagramSelectPage(pageArgs) expect(isValidElement(element)).toBe(true) if (!isValidElement(element)) { throw new Error("InstagramSelectPage did not return a valid element") } - expect(element.props.account).toEqual(account) + + expect(mockResolveConnectSession).toHaveBeenCalledWith({ + userId: "user-1", + sessionId: "session-1", + credentialType: "instagram", + brandingChannel: "instagram", + }) + expect(element.props.account).toEqual({ + avatarUrl: "https://example.com/account.jpg", + id: "ig-1", + name: "Instagram account", + }) + expect(element.props.sessionId).toBe("session-1") expect(element.props.workspaceId).toBe("ws-1") - expect(mockGetInstagramAccount).toHaveBeenCalledWith("user-token") }) - test("redirects to channel creation when the pending-auth cookie is missing or invalid", async () => { - mockReadPendingAuth.mockResolvedValue(null) + test("redirects to channel creation when the session id is missing", async () => { + await expect( + InstagramSelectPage({ searchParams: Promise.resolve({}) }), + ).rejects.toThrow("redirect:/channels/create") - await expect(InstagramSelectPage()).rejects.toThrow( + expect(mockGetCurrentUserId).not.toHaveBeenCalled() + expect(mockResolveConnectSession).not.toHaveBeenCalled() + }) + + test("redirects to channel creation when the user is not authenticated", async () => { + mockGetCurrentUserId.mockResolvedValue(null) + + await expect(InstagramSelectPage(pageArgs)).rejects.toThrow( "redirect:/channels/create", ) - expect(mockGetInstagramAccount).not.toHaveBeenCalled() + + expect(mockResolveConnectSession).not.toHaveBeenCalled() }) - test("redirects to channel creation when no Instagram account is found", async () => { - mockGetInstagramAccount.mockResolvedValue(null) + test("redirects to channel creation when the session has no Instagram account", async () => { + mockResolveConnectSession.mockResolvedValue({ + session: { targets: [] }, + workspace: { id: "ws-1" }, + }) - await expect(InstagramSelectPage()).rejects.toThrow( + await expect(InstagramSelectPage(pageArgs)).rejects.toThrow( "redirect:/channels/create", ) }) diff --git a/apps/builder/__tests__/integration-actions-create-disconnect.test.ts b/apps/builder/__tests__/integration-actions-create-disconnect.test.ts new file mode 100644 index 0000000000..0f9231c2d4 --- /dev/null +++ b/apps/builder/__tests__/integration-actions-create-disconnect.test.ts @@ -0,0 +1,125 @@ +// @vitest-environment node + +import { beforeEach, describe, expect, test, vi } from "vitest" + +const mocks = vi.hoisted(() => ({ + findByProviderSourceId: vi.fn(), + connectionServiceDisconnect: vi.fn(), + loggerError: vi.fn(), +})) + +vi.mock("@chatbotx.io/database/repositories", () => ({ + connectionRepository: { + findByProviderSourceId: mocks.findByProviderSourceId, + }, +})) + +vi.mock("@chatbotx.io/connections", () => ({ + connectionService: { disconnect: mocks.connectionServiceDisconnect }, +})) + +vi.mock("@/lib/log", () => ({ + logger: { error: mocks.loggerError }, +})) + +vi.mock("@/lib/safe-action", () => { + const chain = { + bindArgsSchemas: () => chain, + action: ( + fn: (props: { bindArgsParsedInputs: [string] }) => Promise, + ) => fn, + } + return { workspaceActionClientAllowExpired: chain } +}) + +const { createDisconnectAction } = await import("@/lib/integration-actions") + +beforeEach(() => { + vi.clearAllMocks() +}) + +describe("createDisconnectAction", () => { + test("routes through connectionService.disconnect when a Connection row exists", async () => { + mocks.findByProviderSourceId.mockResolvedValue({ id: "conn-1" }) + const service = { disconnect: vi.fn() } + const action = createDisconnectAction(service, { + name: "ActiveCampaign", + provider: "activeCampaign", + }) as unknown as (props: { + bindArgsParsedInputs: [string] + }) => Promise + + await action({ bindArgsParsedInputs: ["ws-1"] }) + + expect(mocks.findByProviderSourceId).toHaveBeenCalledWith({ + workspaceId: "ws-1", + provider: "activeCampaign", + sourceId: "workspace", + }) + expect(mocks.connectionServiceDisconnect).toHaveBeenCalledWith({ + connectionId: "conn-1", + workspaceId: "ws-1", + }) + expect(service.disconnect).not.toHaveBeenCalled() + }) + + test("falls back to the legacy service.disconnect when no Connection row exists yet (pre-backfill)", async () => { + mocks.findByProviderSourceId.mockResolvedValue(undefined) + const service = { disconnect: vi.fn() } + const action = createDisconnectAction(service, { + name: "ActiveCampaign", + provider: "activeCampaign", + }) as unknown as (props: { + bindArgsParsedInputs: [string] + }) => Promise + + await action({ bindArgsParsedInputs: ["ws-1"] }) + + expect(mocks.connectionServiceDisconnect).not.toHaveBeenCalled() + expect(service.disconnect).toHaveBeenCalledWith("ws-1") + }) + + test("calls afterDisconnect after either path succeeds", async () => { + mocks.findByProviderSourceId.mockResolvedValue({ id: "conn-1" }) + const afterDisconnect = vi.fn() + const action = createDisconnectAction( + { disconnect: vi.fn() }, + { name: "Claude", provider: "claude", afterDisconnect }, + ) as unknown as (props: { bindArgsParsedInputs: [string] }) => Promise + + await action({ bindArgsParsedInputs: ["ws-1"] }) + + expect(afterDisconnect).toHaveBeenCalledWith("ws-1") + }) + + test("logs and rethrows when the underlying disconnect fails", async () => { + mocks.findByProviderSourceId.mockResolvedValue({ id: "conn-1" }) + mocks.connectionServiceDisconnect.mockRejectedValue(new Error("boom")) + const action = createDisconnectAction( + { disconnect: vi.fn() }, + { name: "ActiveCampaign", provider: "activeCampaign" }, + ) as unknown as (props: { bindArgsParsedInputs: [string] }) => Promise + + await expect(action({ bindArgsParsedInputs: ["ws-1"] })).rejects.toThrow( + "boom", + ) + expect(mocks.loggerError).toHaveBeenCalledWith( + expect.objectContaining({ workspaceId: "ws-1" }), + "Failed to disconnect ActiveCampaign", + ) + }) + + test("suppresses error logging when log: false", async () => { + mocks.findByProviderSourceId.mockResolvedValue({ id: "conn-1" }) + mocks.connectionServiceDisconnect.mockRejectedValue(new Error("boom")) + const action = createDisconnectAction( + { disconnect: vi.fn() }, + { name: "Claude", provider: "claude", log: false }, + ) as unknown as (props: { bindArgsParsedInputs: [string] }) => Promise + + await expect(action({ bindArgsParsedInputs: ["ws-1"] })).rejects.toThrow( + "boom", + ) + expect(mocks.loggerError).not.toHaveBeenCalled() + }) +}) diff --git a/apps/builder/__tests__/integration-llm-connect-actions.test.ts b/apps/builder/__tests__/integration-llm-connect-actions.test.ts index 5617c50265..5ba14e501b 100644 --- a/apps/builder/__tests__/integration-llm-connect-actions.test.ts +++ b/apps/builder/__tests__/integration-llm-connect-actions.test.ts @@ -75,7 +75,7 @@ vi.mock("../src/features/integration-claude/lib", () => ({ vi.mock("../src/features/integration-deepseek/lib", () => ({ verifyDeepSeekApiKey: mocks.verifyDeepSeekApiKey, })) -vi.mock("../src/features/integration-ai/lib/verify-api-key", () => ({ +vi.mock("@chatbotx.io/business/integration-ai-provider/verify", () => ({ verifyAiProviderApiKey: mocks.verifyAiProviderApiKey, })) diff --git a/apps/builder/__tests__/integration-webhook-freeze.test.ts b/apps/builder/__tests__/integration-webhook-freeze.test.ts index cb6b6e797f..da87597f6c 100644 --- a/apps/builder/__tests__/integration-webhook-freeze.test.ts +++ b/apps/builder/__tests__/integration-webhook-freeze.test.ts @@ -8,12 +8,14 @@ const findIntegrationTiktokByOpenId = vi.fn() const telegramHandleRequest = vi.fn() const tiktokHandleRequest = vi.fn() const loggerInfo = vi.fn() +const markUnhealthyByIdentifier = vi.fn() vi.mock("@chatbotx.io/business", async () => { const { resolveWorkspaceFreezeReason } = await import( "@chatbotx.io/business/workspace-lifecycle/predicates" ) return { + connectionStateService: { markUnhealthyByIdentifier }, customDomainService: { findActiveByDomain: vi.fn() }, platformCredentialService: { findDecryptedPlatform: vi.fn(), @@ -195,4 +197,25 @@ describe("tiktok webhook freeze", () => { expect(await response.text()).toBe("ok") expect(tiktokHandleRequest).not.toHaveBeenCalled() }) + + test("routes authorization.removed to connectionStateService.markUnhealthyByIdentifier instead of the integration handler", async () => { + const response = await handleWebhook( + "tiktok", + asNextRequest( + "http://localhost/integrations/tiktok", + JSON.stringify({ + user_openid: "open-1", + event: "authorization.removed", + }), + ), + ) + + expect(await response.text()).toBe("ok") + expect(markUnhealthyByIdentifier).toHaveBeenCalledWith({ + provider: "tiktok", + identifier: "open-1", + reason: "token_revoked", + }) + expect(tiktokHandleRequest).not.toHaveBeenCalled() + }) }) diff --git a/apps/builder/__tests__/integrations-public-api.test.ts b/apps/builder/__tests__/integrations-public-api.test.ts index 07df3cd738..136e002405 100644 --- a/apps/builder/__tests__/integrations-public-api.test.ts +++ b/apps/builder/__tests__/integrations-public-api.test.ts @@ -6,6 +6,7 @@ type RouteConfig = { summary: string tags: string[] successStatus?: number + deprecated?: boolean } type CapturedProcedure = { @@ -80,11 +81,6 @@ vi.mock("@chatbotx.io/ai", () => ({ }, })) -const verifyAiProviderApiKey = vi.fn(async () => true) -vi.mock("@/features/integration-ai/lib/verify-api-key", () => ({ - verifyAiProviderApiKey, -})) - const integrationService = { listByWorkspaceId: vi.fn(), findByIdForWorkspace: vi.fn(), @@ -120,6 +116,15 @@ vi.mock("@chatbotx.io/business", () => ({ integrationOpenAIService, })) +const connectionService = { + disconnect: vi.fn(), + connectFromCredentials: vi.fn(), +} +vi.mock("@chatbotx.io/connections", () => ({ connectionService })) + +const connectionRepository = { findByProviderSourceId: vi.fn() } +vi.mock("@chatbotx.io/database/repositories", () => ({ connectionRepository })) + await import("@/features/integrations/api/public/crud") await import("@/features/integrations/api/public/ai") @@ -135,7 +140,6 @@ const findProcedure = (method: string, path: string) => { beforeEach(() => { vi.clearAllMocks() - verifyAiProviderApiKey.mockResolvedValue(true) }) describe("GET /v1/integrations", () => { @@ -292,8 +296,10 @@ describe("GET /v1/integrations/ai/{provider}", () => { describe("PUT /v1/integrations/ai/{provider}", () => { const procedure = findProcedure("PUT", "/v1/integrations/ai/{provider}") - test("connects then returns the resource without the secret", async () => { - integrationGeminiService.connect.mockResolvedValueOnce(undefined) + test("connects via connectionService.connectFromCredentials with allowUpdate, then returns the resource without the secret", async () => { + connectionService.connectFromCredentials.mockResolvedValueOnce({ + id: "conn-1", + }) integrationGeminiService.findByWorkspaceId.mockResolvedValueOnce({ id: "gemini-1", model: "gemini-3.5-flash", @@ -314,18 +320,24 @@ describe("PUT /v1/integrations/ai/{provider}", () => { }, }) - expect(integrationGeminiService.connect).toHaveBeenCalledWith({ + expect(connectionService.connectFromCredentials).toHaveBeenCalledWith({ workspaceId: "workspace-1", - apiKey: "sk-gemini-secret", - model: "gemini-3.5-flash", - temperature: 0.4, - maxOutputTokens: 1024, + provider: "gemini", + config: { + apiKey: "sk-gemini-secret", + model: "gemini-3.5-flash", + temperature: 0.4, + maxOutputTokens: 1024, + }, + allowUpdate: true, }) expect(JSON.stringify(result)).not.toContain("sk-gemini-secret") }) test("invalidates the AI integration cache after connecting", async () => { - integrationGeminiService.connect.mockResolvedValueOnce(undefined) + connectionService.connectFromCredentials.mockResolvedValueOnce({ + id: "conn-1", + }) integrationGeminiService.findByWorkspaceId.mockResolvedValueOnce({ id: "gemini-1", model: "gemini-3.5-flash", @@ -352,8 +364,13 @@ describe("PUT /v1/integrations/ai/{provider}", () => { ) }) - test("rejects an invalid API key without persisting it", async () => { - verifyAiProviderApiKey.mockResolvedValueOnce(false) + test("propagates connectionCredentialsRejected without invalidating the cache", async () => { + connectionService.connectFromCredentials.mockRejectedValueOnce( + new MockChatbotXException( + "Invalid Gemini API key", + "connectionCredentialsRejected", + ), + ) await expect( procedure.handler?.({ @@ -368,29 +385,67 @@ describe("PUT /v1/integrations/ai/{provider}", () => { }), ).rejects.toThrow() - expect(integrationGeminiService.connect).not.toHaveBeenCalled() expect(aiIntegrationService.invalidateCache).not.toHaveBeenCalled() }) + + test("is marked deprecated", () => { + expect(procedure.route.deprecated).toBe(true) + }) }) describe("DELETE /v1/integrations/ai/{provider}", () => { const procedure = findProcedure("DELETE", "/v1/integrations/ai/{provider}") - test("delegates to the provider's disconnect", async () => { - integrationDeepSeekService.disconnect.mockResolvedValueOnce(undefined) + test("resolves the Connection by (workspaceId, provider, 'workspace') and disconnects through connectionService", async () => { + connectionRepository.findByProviderSourceId.mockResolvedValueOnce({ + id: "conn-1", + }) await procedure.handler?.({ context: { workspace: { id: "workspace-1" } }, input: { provider: "deepseek" }, }) - expect(integrationDeepSeekService.disconnect).toHaveBeenCalledWith( + expect(connectionRepository.findByProviderSourceId).toHaveBeenCalledWith({ + workspaceId: "workspace-1", + provider: "deepseek", + sourceId: "workspace", + }) + expect(connectionService.disconnect).toHaveBeenCalledWith({ + connectionId: "conn-1", + workspaceId: "workspace-1", + }) + }) + + test("no-ops the disconnect call (idempotent) when no Connection row exists yet", async () => { + connectionRepository.findByProviderSourceId.mockResolvedValueOnce(undefined) + + await procedure.handler?.({ + context: { workspace: { id: "workspace-1" } }, + input: { provider: "deepseek" }, + }) + + expect(connectionService.disconnect).not.toHaveBeenCalled() + }) + + test("invalidates the AI integration cache after disconnecting", async () => { + connectionRepository.findByProviderSourceId.mockResolvedValueOnce({ + id: "conn-1", + }) + + await procedure.handler?.({ + context: { workspace: { id: "workspace-1" } }, + input: { provider: "deepseek" }, + }) + + expect(aiIntegrationService.invalidateCache).toHaveBeenCalledWith( "workspace-1", + "deepseek", ) }) - test("invalidates the AI integration cache after disconnecting", async () => { - integrationDeepSeekService.disconnect.mockResolvedValueOnce(undefined) + test("invalidates the cache even when there was nothing to disconnect", async () => { + connectionRepository.findByProviderSourceId.mockResolvedValueOnce(undefined) await procedure.handler?.({ context: { workspace: { id: "workspace-1" } }, @@ -406,4 +461,8 @@ describe("DELETE /v1/integrations/ai/{provider}", () => { test("responds with 204 (no body)", () => { expect(procedure.route.successStatus).toBe(204) }) + + test("is marked deprecated", () => { + expect(procedure.route.deprecated).toBe(true) + }) }) diff --git a/apps/builder/__tests__/integrations-public-scope.test.ts b/apps/builder/__tests__/integrations-public-scope.test.ts deleted file mode 100644 index dc593f9e63..0000000000 --- a/apps/builder/__tests__/integrations-public-scope.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -// @vitest-environment node - -import { describe, expect, test, vi } from "vitest" - -// Same rationale as contacts-public-scope.test.ts: importing the real -// integrations public router transitively pulls in -// `@chatbotx.io/database/client` (opens a real `pg.Pool`) and `@/orpc`'s -// `authorizedAPI` chain (boots the full better-auth stack via -// `@/middlewares/auth`). Neither is reachable from this test — it only -// inspects which scope each submodule registered its procedures under — so -// both are stubbed to keep the import side-effect-free. -vi.mock("@/middlewares/auth", () => ({ - authMiddleware: vi.fn(), - workspaceAuthorizedMidddleware: vi.fn(), -})) - -vi.mock("@chatbotx.io/database/client", () => { - const proxy: unknown = new Proxy(() => proxy, { get: () => proxy }) - return { db: proxy } -}) - -const workspaceTokenAuthAPIForScope = vi.hoisted(() => - vi.fn((_scope: string) => { - const chain = { - route: vi.fn(() => chain), - input: vi.fn(() => chain), - output: vi.fn(() => chain), - errors: vi.fn(() => chain), - handler: vi.fn(() => ({})), - } - return chain - }), -) - -vi.mock("@/orpc", () => ({ workspaceTokenAuthAPIForScope })) - -// Each submodule calls `workspaceTokenAuthAPIForScope` exactly once at import -// time — `packages/utils`' Snowflake ID generator is a process-wide singleton -// that throws on re-init, so every submodule is imported exactly once here -// (no `vi.resetModules()` between them) and the full accumulated call list is -// asserted at the end, per submodule slice. -await import("@/features/integrations/api/public/crud") -const crudCallCount = workspaceTokenAuthAPIForScope.mock.calls.length - -await import("@/features/integrations/api/public/ai") -const aiCallCount = workspaceTokenAuthAPIForScope.mock.calls.length - -const allScopeCalls = workspaceTokenAuthAPIForScope.mock.calls.map( - (call) => call[0], -) - -describe("integrations public router scope wiring", () => { - test("crud.ts registers under the 'integrations' scope", () => { - expect(allScopeCalls.slice(0, crudCallCount)).toEqual(["integrations"]) - }) - - test("ai.ts registers under the 'integrations' scope", () => { - expect(allScopeCalls.slice(crudCallCount, aiCallCount)).toEqual([ - "integrations", - ]) - }) -}) diff --git a/apps/builder/__tests__/messaging-ads-oauth-referer-roundtrip.test.ts b/apps/builder/__tests__/messaging-ads-oauth-referer-roundtrip.test.ts index 4903036b0d..3f3d7ebd59 100644 --- a/apps/builder/__tests__/messaging-ads-oauth-referer-roundtrip.test.ts +++ b/apps/builder/__tests__/messaging-ads-oauth-referer-roundtrip.test.ts @@ -130,6 +130,21 @@ vi.mock("@chatbotx.io/database/client", () => ({ db: { transaction: vi.fn() }, })) +vi.mock("@chatbotx.io/business/connect-session", () => ({ + connectSessionService: { + findByNonce: vi.fn(), + fail: vi.fn(), + }, +})) + +vi.mock("@chatbotx.io/connections", () => ({ + connectionService: { + completeAuthorization: vi.fn(), + connectTargets: vi.fn(), + }, + CONNECTION_REGISTRY: {}, +})) + vi.mock("@chatbotx.io/database/schema", () => ({ integrationGoogleSheetsModel: {}, integrationModel: {}, diff --git a/apps/builder/__tests__/messenger-pages.test.tsx b/apps/builder/__tests__/messenger-pages.test.tsx index ac7d4c7a00..6c06ee7d0a 100644 --- a/apps/builder/__tests__/messenger-pages.test.tsx +++ b/apps/builder/__tests__/messenger-pages.test.tsx @@ -127,7 +127,13 @@ describe("MessengerPages", () => { function renderPages(items: MessengerPickerItem[]) { act(() => { - root.render() + root.render( + , + ) }) } @@ -186,7 +192,7 @@ describe("MessengerPages", () => { expect(checkboxes()).toHaveLength(0) }) - test("submitting a single selected item posts only { pageId } to the messenger connect route — no token, no workspaceId", async () => { + test("submitting a single selected item posts only { sessionId, pageId } to the messenger connect route — no token, no workspaceId", async () => { mockConnectViaApi.mockResolvedValue({ kind: "outcome", outcome: { @@ -212,16 +218,16 @@ describe("MessengerPages", () => { }) expect(mockConnectViaApi).toHaveBeenCalledTimes(1) - // Ids only, and the channel's own route — no token, no workspaceId. + // Session and page IDs only, and the channel's own route — no token or workspaceId. expect(mockConnectViaApi.mock.calls[0]?.[0]).toMatchObject({ // The registry entry itself — it now carries the typed oRPC procedure, // so identity is what pins the channel, not a URL string. route: CONNECT_CHANNEL_REGISTRY.messenger.connectRoute, - body: { pageId: "page-selectable" }, + body: { sessionId: "session-1", pageId: "page-selectable" }, }) expect( Object.keys(mockConnectViaApi.mock.calls[0]?.[0]?.body ?? {}), - ).toEqual(["pageId"]) + ).toEqual(["sessionId", "pageId"]) await act(async () => { await Promise.resolve() diff --git a/apps/builder/__tests__/messenger-select-page-action.test.ts b/apps/builder/__tests__/messenger-select-page-action.test.ts index 68e7db89d5..f9dea8fe6c 100644 --- a/apps/builder/__tests__/messenger-select-page-action.test.ts +++ b/apps/builder/__tests__/messenger-select-page-action.test.ts @@ -6,75 +6,42 @@ import { credentialMissingException, notWorkspaceMemberException, } from "@chatbotx.io/business/errors" -import { SdkException } from "@chatbotx.io/sdk" import { beforeEach, describe, expect, test, vi } from "vitest" -// --------------------------------------------------------------------------- -// `connectMessengerPage` follows the shared per-account connect skeleton (plan -// §2.4): plan §2.4 steps 1-4 (pending-auth cookie -> workspace/membership -> -// owner gate -> credential + branding) are delegated to the shared -// `resolveConnectSession` helper (see `resolve-connect-session.test.ts` for -// that helper's own branch coverage) — mocked here as one function so this -// file only asserts the ACTION's behavior: it never throws, and every -// session-level failure `resolveConnectSession` raises is correctly mapped -// through `toConnectSessionError` before any provider/persist call runs. -// `getUserPages` runs once per request (no cache — provider lists carry -// page access tokens, §4.9). -// --------------------------------------------------------------------------- - const { buildContextMock, - connectPageMock, + connectTargetsMock, enqueueChannelScanMock, - exchangeLongLivedTokenMock, - findConnectedPageIdsMock, - getUserPagesMock, - loggerWarnMock, + findByInboxIdMock, loggerErrorMock, - persistIntegrationUserInfoMock, + loggerWarnMock, resolveConnectSessionMock, runChannelHandlerMock, - subscribePageToAppWebhookMock, - updateUserInfoMock, updateWorkspaceLogoMock, } = vi.hoisted(() => ({ buildContextMock: vi.fn(), - connectPageMock: vi.fn(), + connectTargetsMock: vi.fn(), enqueueChannelScanMock: vi.fn(), - exchangeLongLivedTokenMock: vi.fn(), - findConnectedPageIdsMock: vi.fn(), - getUserPagesMock: vi.fn(), - loggerWarnMock: vi.fn(), + findByInboxIdMock: vi.fn(), loggerErrorMock: vi.fn(), - persistIntegrationUserInfoMock: vi.fn(), + loggerWarnMock: vi.fn(), resolveConnectSessionMock: vi.fn(), runChannelHandlerMock: vi.fn(), - subscribePageToAppWebhookMock: vi.fn(), - updateUserInfoMock: vi.fn(), updateWorkspaceLogoMock: vi.fn(), })) -vi.mock("@/lib/facebook-pending-auth", () => ({ - FB_MESSENGER_PENDING_AUTH_COOKIE: "fb_messenger_pending_auth", -})) - vi.mock("@/features/channel-connect/lib/resolve-connect-session", () => ({ resolveConnectSession: resolveConnectSessionMock, })) vi.mock("@/features/integration-webchat/lib", () => ({ BRANDING_TITLE: "ChatbotX", - getBrandingUrl: () => "https://app.test/branding", })) vi.mock("@/features/workspaces/actions/upload-logo", () => ({ updateWorkspaceLogo: updateWorkspaceLogoMock, })) -vi.mock("@/lib/integration-user-info", () => ({ - persistIntegrationUserInfo: persistIntegrationUserInfoMock, -})) - vi.mock("@/lib/log", () => ({ logger: { warn: loggerWarnMock, error: loggerErrorMock, info: vi.fn() }, })) @@ -82,26 +49,17 @@ vi.mock("@/lib/log", () => ({ vi.mock("@chatbotx.io/business", () => ({ buildContext: buildContextMock, messengerIntegrationService: { - findConnectedPageIds: findConnectedPageIdsMock, - connectPage: connectPageMock, - updateUserInfo: updateUserInfoMock, + findByInboxId: findByInboxIdMock, }, tagSyncService: { enqueueChannelScan: enqueueChannelScanMock }, })) -vi.mock("@chatbotx.io/integration-messenger", () => ({ - getUserPages: getUserPagesMock, - integration: { runChannelHandler: runChannelHandlerMock }, +vi.mock("@chatbotx.io/connections", () => ({ + connectionService: { connectTargets: connectTargetsMock }, })) -vi.mock("@chatbotx.io/integration-messenger/apis/page", () => ({ - exchangeLongLivedToken: exchangeLongLivedTokenMock, - subscribePageToAppWebhook: subscribePageToAppWebhookMock, -})) - -vi.mock("@chatbotx.io/sdk", () => ({ - AuthType: { oauth2: "oauth2" }, - SdkException: class SdkException extends Error {}, +vi.mock("@chatbotx.io/integration-messenger", () => ({ + integration: { runChannelHandler: runChannelHandlerMock }, })) const { connectMessengerPage } = await import( @@ -110,41 +68,18 @@ const { connectMessengerPage } = await import( const call = connectMessengerPage -const connectablePage = { - id: "p1", - name: "Page One", - access_token: "page-token-1", - isConnectable: true, -} - -const notAdminPage = { - id: "p1", - name: "Page One", - access_token: "page-token-1", - isConnectable: false, -} - const resolvedSession = { - pendingAuth: { - userToken: "user-token-1", - userId: "fb-user-1", - userName: "FB User", - userAvatarUrl: "https://example.com/avatar.jpg", - workspaceId: "ws-1", - referer: "/channels/create", - version: "v23.0", - expiresAt: Date.now() + 600_000, + session: { + targets: [ + { + id: "p1", + name: "Page One", + selectable: true, + }, + ], }, workspace: { id: "ws-1", ownerId: "owner-1" }, platformOwnerId: "owner-1", - credential: { - config: { - clientId: "client-1", - clientSecret: "secret-1", - version: "v23.0", - }, - }, - appUrl: "https://app.test", brandingMenuEntry: { label: "ChatbotX", type: "url" as const, @@ -157,27 +92,21 @@ describe("connectMessengerPage", () => { vi.clearAllMocks() resolveConnectSessionMock.mockResolvedValue(resolvedSession) - getUserPagesMock.mockResolvedValue({ - pages: [connectablePage], - bmLookupFailed: false, + connectTargetsMock.mockResolvedValue({ + outcomes: [{ sourceId: "p1", status: "connected" }], + connections: [{ inboxId: "inbox-1" }], }) - findConnectedPageIdsMock.mockResolvedValue(new Set()) - exchangeLongLivedTokenMock.mockResolvedValue("long-lived-token") - subscribePageToAppWebhookMock.mockResolvedValue(undefined) - connectPageMock.mockResolvedValue({ - workspaceId: "ws-1", - integrationId: "integration-1", - wasCreated: true, - integration: { id: "integration-1", workspaceId: "ws-1" }, + findByInboxIdMock.mockResolvedValue({ + id: "integration-1", + auth: {}, }) runChannelHandlerMock.mockResolvedValue(undefined) updateWorkspaceLogoMock.mockResolvedValue(undefined) - persistIntegrationUserInfoMock.mockResolvedValue(undefined) enqueueChannelScanMock.mockResolvedValue(undefined) buildContextMock.mockResolvedValue({}) }) - test("returns sessionExpired and touches nothing else when the pending-auth cookie is missing/invalid", async () => { + test("returns sessionExpired and touches nothing else when the connect session is missing/invalid", async () => { resolveConnectSessionMock.mockRejectedValue( new ChatbotXException( "Your connect session expired. Please start again.", @@ -187,23 +116,25 @@ describe("connectMessengerPage", () => { const result = await call({ userId: "user-1", + sessionId: "session-1", pageId: "p1", }) expect(result).toEqual({ kind: "sessionError", code: "sessionExpired" }) - expect(getUserPagesMock).not.toHaveBeenCalled() + expect(connectTargetsMock).not.toHaveBeenCalled() }) - test("returns notMember before any provider call when the resolver rejects membership", async () => { + test("returns notMember before connecting when the resolver rejects membership", async () => { resolveConnectSessionMock.mockRejectedValue(notWorkspaceMemberException()) const result = await call({ userId: "user-1", + sessionId: "session-1", pageId: "p1", }) expect(result).toEqual({ kind: "sessionError", code: "notMember" }) - expect(getUserPagesMock).not.toHaveBeenCalled() + expect(connectTargetsMock).not.toHaveBeenCalled() }) test("returns trialExpired when the workspace owner is blocked", async () => { @@ -213,11 +144,12 @@ describe("connectMessengerPage", () => { const result = await call({ userId: "user-1", + sessionId: "session-1", pageId: "p1", }) expect(result).toEqual({ kind: "sessionError", code: "trialExpired" }) - expect(getUserPagesMock).not.toHaveBeenCalled() + expect(connectTargetsMock).not.toHaveBeenCalled() }) test("returns macLimitReached when the workspace owner is blocked on MAC", async () => { @@ -231,6 +163,7 @@ describe("connectMessengerPage", () => { const result = await call({ userId: "user-1", + sessionId: "session-1", pageId: "p1", }) @@ -246,16 +179,18 @@ describe("connectMessengerPage", () => { const result = await call({ userId: "user-1", + sessionId: "session-1", pageId: "p1", }) expect(result).toEqual({ kind: "sessionError", code: "credentialMissing" }) - expect(getUserPagesMock).not.toHaveBeenCalled() + expect(connectTargetsMock).not.toHaveBeenCalled() }) - test("a forged/unknown page id resolves to notSelectable without any Meta call", async () => { + test("a forged/unknown page id resolves to notSelectable without connecting", async () => { const result = await call({ userId: "user-1", + sessionId: "session-1", pageId: "forged-id", }) @@ -269,19 +204,20 @@ describe("connectMessengerPage", () => { coexistEligible: false, }, }) - expect(exchangeLongLivedTokenMock).not.toHaveBeenCalled() - expect(subscribePageToAppWebhookMock).not.toHaveBeenCalled() - expect(connectPageMock).not.toHaveBeenCalled() + expect(connectTargetsMock).not.toHaveBeenCalled() }) - test("a non-admin page id resolves to notSelectable without any Meta call", async () => { - getUserPagesMock.mockResolvedValue({ - pages: [notAdminPage], - bmLookupFailed: false, + test("a non-selectable page resolves to notSelectable without connecting", async () => { + resolveConnectSessionMock.mockResolvedValue({ + ...resolvedSession, + session: { + targets: [{ id: "p1", name: "Page One", selectable: false }], + }, }) const result = await call({ userId: "user-1", + sessionId: "session-1", pageId: "p1", }) @@ -295,14 +231,27 @@ describe("connectMessengerPage", () => { coexistEligible: false, }, }) - expect(exchangeLongLivedTokenMock).not.toHaveBeenCalled() + expect(connectTargetsMock).not.toHaveBeenCalled() }) - test("an already-connected page resolves to duplicated without any further Meta call", async () => { - findConnectedPageIdsMock.mockResolvedValue(new Set(["p1"])) + test("an already-connected page resolves to duplicated without connecting", async () => { + resolveConnectSessionMock.mockResolvedValue({ + ...resolvedSession, + session: { + targets: [ + { + id: "p1", + name: "Page One", + selectable: false, + alreadyConnected: "this_workspace", + }, + ], + }, + }) const result = await call({ userId: "user-1", + sessionId: "session-1", pageId: "p1", }) @@ -316,13 +265,13 @@ describe("connectMessengerPage", () => { coexistEligible: false, }, }) - expect(exchangeLongLivedTokenMock).not.toHaveBeenCalled() - expect(connectPageMock).not.toHaveBeenCalled() + expect(connectTargetsMock).not.toHaveBeenCalled() }) - test("happy path: getUserPages -> exchange -> subscribe -> persist -> follow-ups, one Graph list call per request", async () => { + test("connects the selected target and runs Messenger follow-ups", async () => { const result = await call({ userId: "user-1", + sessionId: "session-1", pageId: "p1", }) @@ -337,52 +286,34 @@ describe("connectMessengerPage", () => { coexistEligible: true, }, }) - - expect(getUserPagesMock).toHaveBeenCalledTimes(1) - expect(exchangeLongLivedTokenMock).toHaveBeenCalledWith( - expect.objectContaining({ clientId: "client-1" }), - "page-token-1", - ) - expect(subscribePageToAppWebhookMock).toHaveBeenCalledWith( - expect.objectContaining({ - pageId: "p1", - accessToken: "long-lived-token", - }), - ) - expect(connectPageMock).toHaveBeenCalledWith( - expect.objectContaining({ - actorUserId: "user-1", - ownerId: "owner-1", - workspaceId: "ws-1", - page: { pageId: "p1", pageName: "Page One" }, - }), - ) + expect(connectTargetsMock).toHaveBeenCalledWith({ + sessionId: "session-1", + workspaceId: "ws-1", + targetIds: ["p1"], + actorUserId: "user-1", + }) expect(enqueueChannelScanMock).toHaveBeenCalledWith({ workspaceId: "ws-1", channelType: "messenger", integrationId: "integration-1", }) - - const order = [ - getUserPagesMock, - exchangeLongLivedTokenMock, - subscribePageToAppWebhookMock, - connectPageMock, - runChannelHandlerMock, - updateWorkspaceLogoMock, - persistIntegrationUserInfoMock, - enqueueChannelScanMock, - ].map((mock) => mock.mock.invocationCallOrder[0]) - expect(order).toEqual([...order].sort((a, b) => a - b)) }) - test("a subscribe-webhook failure resolves to providerRejected and never persists", async () => { - subscribePageToAppWebhookMock.mockRejectedValue( - new SdkException("Meta rejected the subscription"), - ) + test("a failed connection outcome is returned without follow-ups", async () => { + connectTargetsMock.mockResolvedValue({ + outcomes: [ + { + sourceId: "p1", + status: "failed", + reason: "providerRejected", + }, + ], + connections: [], + }) const result = await call({ userId: "user-1", + sessionId: "session-1", pageId: "p1", }) @@ -396,7 +327,7 @@ describe("connectMessengerPage", () => { coexistEligible: false, }, }) - expect(connectPageMock).not.toHaveBeenCalled() + expect(runChannelHandlerMock).not.toHaveBeenCalled() }) test("a follow-up failure still returns a connected outcome, carrying a followUpFailed warning", async () => { @@ -404,6 +335,7 @@ describe("connectMessengerPage", () => { const result = await call({ userId: "user-1", + sessionId: "session-1", pageId: "p1", }) @@ -421,11 +353,12 @@ describe("connectMessengerPage", () => { expect(loggerWarnMock).toHaveBeenCalled() }) - test("a unique-violation race during persist resolves to duplicated", async () => { - connectPageMock.mockRejectedValue(channelDuplicatedException()) + test("a unique-violation race during connection resolves to duplicated", async () => { + connectTargetsMock.mockRejectedValue(channelDuplicatedException()) const result = await call({ userId: "user-1", + sessionId: "session-1", pageId: "p1", }) diff --git a/apps/builder/__tests__/messenger-select-page.test.tsx b/apps/builder/__tests__/messenger-select-page.test.tsx index 7e4b29df22..d15e87b385 100644 --- a/apps/builder/__tests__/messenger-select-page.test.tsx +++ b/apps/builder/__tests__/messenger-select-page.test.tsx @@ -1,22 +1,19 @@ // @vitest-environment node -import type { ConnectableFacebookPage } from "@chatbotx.io/integration-messenger/schema" import { isValidElement } from "react" import { beforeEach, describe, expect, test, vi } from "vitest" const { - mockFindConnectedMessengerPageIds, - mockGetUserPages, - mockReadPendingAuth, + mockGetCurrentUserId, mockRedirect, + mockResolveConnectSession, mockSelectPage, } = vi.hoisted(() => ({ - mockFindConnectedMessengerPageIds: vi.fn(), - mockGetUserPages: vi.fn(), - mockReadPendingAuth: vi.fn(), + mockGetCurrentUserId: vi.fn(), mockRedirect: vi.fn((path: string) => { throw new Error(`redirect:${path}`) }), + mockResolveConnectSession: vi.fn(), mockSelectPage: vi.fn(() => null), })) @@ -29,19 +26,12 @@ vi.mock("next-intl/server", () => ({ getTranslations: async () => (key: string) => key, })) -vi.mock("@chatbotx.io/business", () => ({ - messengerIntegrationService: { - findConnectedPageIds: mockFindConnectedMessengerPageIds, - }, +vi.mock("@/lib/auth/utils", () => ({ + getCurrentUserId: mockGetCurrentUserId, })) -vi.mock("@chatbotx.io/integration-messenger", () => ({ - getUserPages: mockGetUserPages, -})) - -vi.mock("@/lib/facebook-pending-auth", () => ({ - readPendingAuth: mockReadPendingAuth, - FB_MESSENGER_PENDING_AUTH_COOKIE: "fb_messenger_pending_auth", +vi.mock("@/features/channel-connect/lib/resolve-connect-session", () => ({ + resolveConnectSession: mockResolveConnectSession, })) vi.mock("@/features/inboxes/components/inbox-icon", () => ({ @@ -57,6 +47,7 @@ const { default: MessengerSelectPage } = await import( ) type SelectPageElementProps = { + bmLookupFailed: boolean items: Array<{ id: string isAlreadyConnected: boolean @@ -65,71 +56,55 @@ type SelectPageElementProps = { disabledReason?: string secondary?: string }> + sessionId: string + workspaceId: string } -const connectablePage: ConnectableFacebookPage = { - id: "page-connectable", - name: "Connectable Page", - access_token: "connectable-token", - isConnectable: true, +const pageArgs = { + searchParams: Promise.resolve({ session: "session-1" }), } -const notAdminPage: ConnectableFacebookPage = { - id: "page-not-admin", - name: "Not Admin Page", - access_token: "not-admin-token", - isConnectable: false, +const connectableTarget = { + id: "page-connectable", + name: "Connectable Page", + selectable: true, } -const alreadyConnectedPage: ConnectableFacebookPage = { +const alreadyConnectedTarget = { id: "page-connected", name: "Connected Page", - access_token: "connected-token", - isConnectable: false, -} - -// Meta can report a page as both connect-eligible and already connected -// elsewhere (e.g. reconnected under a different workspace) — this must still -// rank last and be treated as disabled, exactly like any other -// already-connected page. -const connectableAndConnectedPage: ConnectableFacebookPage = { - id: "page-connectable-and-connected", - name: "Connectable But Connected Page", - access_token: "connectable-and-connected-token", - isConnectable: true, + selectable: false, + alreadyConnected: "other_workspace" as const, } describe("MessengerSelectPage", () => { beforeEach(() => { vi.clearAllMocks() - mockReadPendingAuth.mockResolvedValue({ - userToken: "user-token", - version: "v23.0", - referer: "/channels/create", - workspaceId: "ws-1", - }) - mockGetUserPages.mockResolvedValue({ - pages: [ - notAdminPage, - alreadyConnectedPage, - connectablePage, - connectableAndConnectedPage, - ], - bmLookupFailed: false, + mockGetCurrentUserId.mockResolvedValue("user-1") + mockResolveConnectSession.mockResolvedValue({ + session: { + targets: [alreadyConnectedTarget, connectableTarget], + }, + workspace: { id: "ws-1" }, }) - mockFindConnectedMessengerPageIds.mockResolvedValue( - new Set(["page-connected", "page-connectable-and-connected"]), - ) }) - test("passes every page through as a picker item, ranked connectable first, then non-admin, then already-connected", async () => { - const element = await MessengerSelectPage() + test("passes session targets through as picker items, ranked selectable first then already-connected", async () => { + const element = await MessengerSelectPage(pageArgs) expect(isValidElement(element)).toBe(true) if (!isValidElement(element)) { throw new Error("MessengerSelectPage did not return a valid element") } + expect(mockResolveConnectSession).toHaveBeenCalledWith({ + userId: "user-1", + sessionId: "session-1", + credentialType: "messenger", + brandingChannel: "messenger", + }) + expect(element.props.sessionId).toBe("session-1") + expect(element.props.workspaceId).toBe("ws-1") expect(element.props.items).toEqual([ expect.objectContaining({ id: "page-connectable", @@ -137,22 +112,8 @@ describe("MessengerSelectPage", () => { isAlreadyConnected: false, disabled: false, }), - expect.objectContaining({ - id: "page-not-admin", - isConnectable: false, - isAlreadyConnected: false, - disabled: true, - disabledReason: "messenger.selectPage.notAdminNote", - }), expect.objectContaining({ id: "page-connected", - isConnectable: false, - isAlreadyConnected: true, - disabled: true, - disabledReason: "messenger.selectPage.alreadyConnectedNote", - }), - expect.objectContaining({ - id: "page-connectable-and-connected", isConnectable: true, isAlreadyConnected: true, disabled: true, @@ -161,20 +122,24 @@ describe("MessengerSelectPage", () => { ]) }) - test("never sends access_token to the client for any page", async () => { - const element = await MessengerSelectPage() + test("renders an empty picker when the session has no targets", async () => { + mockResolveConnectSession.mockResolvedValue({ + session: { targets: [] }, + workspace: { id: "ws-1" }, + }) + + const element = await MessengerSelectPage(pageArgs) if (!isValidElement(element)) { throw new Error("MessengerSelectPage did not return a valid element") } - for (const item of element.props.items) { - expect(item).not.toHaveProperty("access_token") - } + expect(mockRedirect).not.toHaveBeenCalled() + expect(element.props.items).toEqual([]) }) test("uses the page id as the secondary line", async () => { - const element = await MessengerSelectPage() + const element = await MessengerSelectPage(pageArgs) if (!isValidElement(element)) { throw new Error("MessengerSelectPage did not return a valid element") @@ -186,12 +151,34 @@ describe("MessengerSelectPage", () => { expect(connectable?.secondary).toBe("page-connectable") }) - test("redirects to channel creation when the pending-auth cookie is missing or invalid", async () => { - mockReadPendingAuth.mockResolvedValue(null) + // Non-admin pages are filtered before session targets are created, so the + // old not-admin warning cannot structurally render in this picker anymore. + test("does not set the legacy Business Manager lookup warning", async () => { + const element = await MessengerSelectPage(pageArgs) - await expect(MessengerSelectPage()).rejects.toThrow( + if (!isValidElement(element)) { + throw new Error("MessengerSelectPage did not return a valid element") + } + + expect(element.props.bmLookupFailed).toBe(false) + }) + + test("redirects to channel creation when the session id is missing", async () => { + await expect( + MessengerSelectPage({ searchParams: Promise.resolve({}) }), + ).rejects.toThrow("redirect:/channels/create") + + expect(mockGetCurrentUserId).not.toHaveBeenCalled() + expect(mockResolveConnectSession).not.toHaveBeenCalled() + }) + + test("redirects to channel creation when the user is not authenticated", async () => { + mockGetCurrentUserId.mockResolvedValue(null) + + await expect(MessengerSelectPage(pageArgs)).rejects.toThrow( "redirect:/channels/create", ) - expect(mockGetUserPages).not.toHaveBeenCalled() + + expect(mockResolveConnectSession).not.toHaveBeenCalled() }) }) diff --git a/apps/builder/__tests__/oauth-callback-connect-session.test.ts b/apps/builder/__tests__/oauth-callback-connect-session.test.ts new file mode 100644 index 0000000000..37cd18b6c1 --- /dev/null +++ b/apps/builder/__tests__/oauth-callback-connect-session.test.ts @@ -0,0 +1,428 @@ +// @vitest-environment node + +import type { NextRequest } from "next/server" +import { beforeEach, describe, expect, test, vi } from "vitest" + +const { + mockFindByNonce, + mockFailSession, + mockCompleteAuthorization, + mockConnectTargets, + mockResolveForOwner, + mockSanitizeReferer, + mockResolveRelayTarget, + mockNotFound, + mockRedirect, + mockGetCurrentUser, +} = vi.hoisted(() => ({ + mockFindByNonce: vi.fn(), + mockFailSession: vi.fn(), + mockCompleteAuthorization: vi.fn(), + mockConnectTargets: vi.fn(), + mockResolveForOwner: vi.fn(), + mockSanitizeReferer: vi.fn(async (referer: string) => referer), + mockResolveRelayTarget: vi.fn(async () => null), + mockNotFound: vi.fn(() => { + throw new Error("not found") + }), + mockRedirect: vi.fn((target: string) => target), + mockGetCurrentUser: vi.fn(), +})) + +vi.mock("@chatbotx.io/business", () => ({ + platformCredentialService: { resolveForOwner: mockResolveForOwner }, + hasWorkspaceAccess: vi.fn(), + workspaceService: { findById: vi.fn(), create: vi.fn() }, + messengerIntegrationService: {}, + instagramIntegrationService: {}, + integrationWhatsappService: {}, + messagingAdsConnectionService: {}, + appointmentExternalCalendarService: {}, + integrationFacebookAdsService: {}, + integrationMetaCatalogService: {}, +})) + +vi.mock("@chatbotx.io/business/audit", () => ({ + auditService: { record: vi.fn() }, + withAuditContext: async (_ctx: unknown, fn: () => Promise) => + await fn(), +})) + +vi.mock("@chatbotx.io/business/connect-session", () => ({ + connectSessionService: { + findByNonce: mockFindByNonce, + fail: mockFailSession, + }, +})) + +vi.mock("@chatbotx.io/connections", () => ({ + connectionService: { + completeAuthorization: mockCompleteAuthorization, + connectTargets: mockConnectTargets, + }, + CONNECTION_REGISTRY: { + messenger: { + credentialType: "messenger", + provider: { multiAccount: true }, + }, + zalo: { + credentialType: "zalo", + provider: { multiAccount: false }, + }, + unconfigured: null, + }, +})) + +vi.mock("@chatbotx.io/database/client", () => ({ + db: { transaction: vi.fn() }, +})) + +vi.mock("@chatbotx.io/database/schema", () => ({ + integrationGoogleSheetsModel: {}, + integrationModel: {}, + ROOT_TENANT_ID: "1", +})) + +vi.mock("@chatbotx.io/integration-facebook-ads", () => ({ + exchangeCodeForToken: vi.fn(), + exchangeLongLivedToken: vi.fn(), +})) +vi.mock("@chatbotx.io/integration-instagram", () => ({ + exchangeCodeForToken: vi.fn(), + getInstagramAccount: vi.fn(), + subscribePageToInstagramWebhook: vi.fn(), +})) +vi.mock("@chatbotx.io/integration-instagram-facebook", () => ({ + exchangeCodeForToken: vi.fn(), + getFacebookUser: vi.fn(), + getUserInstagramAccounts: vi.fn(), + subscribePageToInstagramWebhook: vi.fn(), +})) +vi.mock("@chatbotx.io/integration-messenger", () => ({ + exchangeCodeForToken: vi.fn(), + getFacebookUser: vi.fn(), + getUserPages: vi.fn(), +})) +vi.mock("@chatbotx.io/integration-messenger/apis/page", () => ({ + exchangeLongLivedToken: vi.fn(), + subscribePageToAppWebhook: vi.fn(), +})) + +vi.mock("@chatbotx.io/sdk", () => ({ + AuthType: { oauth2: "oauth2", custom: "custom" }, + SdkException: class SdkException extends Error {}, +})) + +vi.mock("@chatbotx.io/utils", async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + getPublicUrlFromRequest: (request: { url: string }) => request.url, + } +}) + +vi.mock("next/navigation", () => ({ + notFound: mockNotFound, + redirect: mockRedirect, +})) + +vi.mock("@/features/integration-messenger/actions/reconnect-callback", () => ({ + reconnectMessengerHandler: vi.fn(), +})) +vi.mock("@/features/integration-instagram/actions/reconnect-callback", () => ({ + reconnectInstagramHandler: vi.fn(), + reconnectInstagramFacebookHandler: vi.fn(), +})) +vi.mock("@/features/external-calendars/lib/google-calendar-provider", () => ({ + exchangeAndVerifyGoogleCalendar: vi.fn(), +})) +vi.mock("@/features/integration-tiktok/actions/connect.action", () => ({ + connectTiktokHandler: vi.fn(), +})) +vi.mock("@/features/integration-zalo/actions/connect-zalo.action", () => ({ + connectZaloHandler: vi.fn(), +})) +vi.mock("@/features/integration-zalo/actions/reconnect-callback", () => ({ + reconnectZaloHandler: vi.fn(), +})) + +vi.mock("@/integration", () => ({ + integrations: { + messenger: {}, + instagram: {}, + instagramFacebook: {}, + facebookAds: {}, + tiktok: {}, + zalo: {}, + googleCalendar: {}, + googleSheets: {}, + }, +})) + +vi.mock("@/lib/platform-credential-owner", () => ({ + resolveOwnerForWorkspace: vi.fn(async () => "platform-owner-1"), +})) + +vi.mock("@/lib/auth/utils", () => ({ getCurrentUser: mockGetCurrentUser })) + +vi.mock("@/lib/log", () => ({ + logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() }, +})) + +vi.mock("@/lib/oauth-broker", () => ({ + buildBrokerCallbackUrl: (path: string) => `https://broker.example.com${path}`, + getBrokerOrigin: () => "https://broker.example.com", +})) + +vi.mock("@/lib/oauth-referer", () => ({ + resolveRelayTarget: mockResolveRelayTarget, + sanitizeReferer: mockSanitizeReferer, +})) + +const { handleCallback } = await import( + "../src/app/integrations/[...integration]/callback" +) + +const buildRequest = (state: string, extraParams = "") => + ({ + headers: new Headers(), + url: `https://app.example.com/integrations/messenger/callback?code=code-1&state=${encodeURIComponent(state)}${extraParams}`, + }) as unknown as NextRequest + +beforeEach(() => { + vi.clearAllMocks() + mockResolveForOwner.mockResolvedValue({ + config: { clientId: "id", clientSecret: "secret" }, + userId: null, + }) +}) + +describe("handleCallback — ConnectSession state dispatch", () => { + test("a raw sessionId.nonce state still parses as valid JSON/base64 gets routed to the legacy switch and 404s for messenger without a workspaceId/user session", async () => { + // Confirms the dispatch regex is specific: legacy base64-JSON states + // never accidentally match `^\d+\.[A-Za-z0-9_-]+$` (e.g. plain digits + // followed by more base64 chars containing "="/"+"/"/" would not match). + mockGetCurrentUser.mockResolvedValue(null) + const legacyState = Buffer.from( + JSON.stringify({ referer: "https://app.example.com/manage" }), + ).toString("base64") + + await expect( + handleCallback("messenger", buildRequest(legacyState)), + ).rejects.toThrow("not found") + + expect(mockFindByNonce).not.toHaveBeenCalled() + }) + + test("resolves the session by nonce, verifies the id matches, and 404s on mismatch", async () => { + mockFindByNonce.mockResolvedValueOnce({ id: "999", returnUrl: null }) + + await expect( + handleCallback("messenger", buildRequest("123.abc-nonce")), + ).rejects.toThrow("not found") + + expect(mockFindByNonce).toHaveBeenCalledWith("abc-nonce") + expect(mockCompleteAuthorization).not.toHaveBeenCalled() + }) + + test("404s when no session resolves for the nonce", async () => { + mockFindByNonce.mockResolvedValueOnce(undefined) + + await expect( + handleCallback("messenger", buildRequest("123.abc-nonce")), + ).rejects.toThrow("not found") + }) + + test("provider denial (?error=) fails the session and redirects without exchanging code", async () => { + mockFindByNonce.mockResolvedValueOnce({ + id: "123", + provider: "messenger", + returnUrl: null, + platformOwnerId: "owner-1", + }) + + await handleCallback( + "messenger", + buildRequest("123.abc-nonce", "&error=access_denied"), + ) + + expect(mockFailSession).toHaveBeenCalledWith({ + id: "123", + errorCode: "provider_denied", + }) + expect(mockCompleteAuthorization).not.toHaveBeenCalled() + expect(mockRedirect).toHaveBeenCalledWith("/connect/123") + }) + + test("redirects to the session's own sanitized returnUrl when set", async () => { + mockFindByNonce.mockResolvedValueOnce({ + id: "123", + provider: "messenger", + returnUrl: "https://app.example.com/done", + platformOwnerId: "owner-1", + }) + mockCompleteAuthorization.mockResolvedValueOnce({ + id: "123", + workspaceId: "ws-1", + status: "completed", + targets: [], + }) + + await handleCallback("messenger", buildRequest("123.abc-nonce")) + + expect(mockSanitizeReferer).toHaveBeenCalledWith( + "https://app.example.com/done", + ) + expect(mockRedirect).toHaveBeenCalledWith("https://app.example.com/done") + }) + + test("404s when the session's provider has no registered credentialType", async () => { + mockFindByNonce.mockResolvedValueOnce({ + id: "123", + provider: "unconfigured", + returnUrl: null, + platformOwnerId: "owner-1", + }) + + await expect( + handleCallback("messenger", buildRequest("123.abc-nonce")), + ).rejects.toThrow("not found") + expect(mockResolveForOwner).not.toHaveBeenCalled() + }) + + test("404s when the session has no platformOwnerId recorded", async () => { + mockFindByNonce.mockResolvedValueOnce({ + id: "123", + provider: "messenger", + returnUrl: null, + platformOwnerId: null, + }) + + await expect( + handleCallback("messenger", buildRequest("123.abc-nonce")), + ).rejects.toThrow("not found") + }) + + test("404s when the platform credential cannot be resolved", async () => { + mockFindByNonce.mockResolvedValueOnce({ + id: "123", + provider: "messenger", + returnUrl: null, + platformOwnerId: "owner-1", + }) + mockResolveForOwner.mockResolvedValueOnce(undefined) + + await expect( + handleCallback("messenger", buildRequest("123.abc-nonce")), + ).rejects.toThrow("not found") + expect(mockCompleteAuthorization).not.toHaveBeenCalled() + }) + + test("calls completeAuthorization with the reconstructed callback URL and this exact credential config", async () => { + mockFindByNonce.mockResolvedValueOnce({ + id: "123", + provider: "messenger", + returnUrl: null, + platformOwnerId: "owner-1", + }) + mockResolveForOwner.mockResolvedValueOnce({ + config: { clientId: "client-9", clientSecret: "secret-9" }, + userId: null, + }) + mockCompleteAuthorization.mockResolvedValueOnce({ + id: "123", + workspaceId: "ws-1", + status: "awaiting_selection", + targets: [{ id: "page-1", selectable: false }], + }) + + await handleCallback("messenger", buildRequest("123.abc-nonce")) + + expect(mockCompleteAuthorization).toHaveBeenCalledWith({ + sessionId: "123", + nonce: "abc-nonce", + code: "code-1", + callbackUrl: "https://app.example.com/integrations/messenger/callback", + credential: { clientId: "client-9", clientSecret: "secret-9" }, + }) + }) + + test("auto-completes a non-multiAccount provider's single selectable target via connectTargets", async () => { + mockFindByNonce.mockResolvedValueOnce({ + id: "123", + provider: "zalo", + returnUrl: null, + platformOwnerId: "owner-1", + }) + mockCompleteAuthorization.mockResolvedValueOnce({ + id: "123", + workspaceId: "ws-1", + status: "awaiting_selection", + targets: [{ id: "oa-1", selectable: true }], + }) + + await handleCallback("messenger", buildRequest("123.abc-nonce")) + + expect(mockConnectTargets).toHaveBeenCalledWith({ + sessionId: "123", + workspaceId: "ws-1", + targetIds: ["oa-1"], + }) + }) + + test("does not auto-complete a multiAccount provider's awaiting_selection session", async () => { + mockFindByNonce.mockResolvedValueOnce({ + id: "123", + provider: "messenger", + returnUrl: null, + platformOwnerId: "owner-1", + }) + mockCompleteAuthorization.mockResolvedValueOnce({ + id: "123", + workspaceId: "ws-1", + status: "awaiting_selection", + targets: [{ id: "page-1", selectable: true }], + }) + + await handleCallback("messenger", buildRequest("123.abc-nonce")) + + expect(mockConnectTargets).not.toHaveBeenCalled() + }) + + test("does not auto-complete a builder-initiated (actorUserId) session even when non-multiAccount — its own picker confirm screen finishes the connect", async () => { + mockFindByNonce.mockResolvedValueOnce({ + id: "123", + provider: "zalo", + returnUrl: "/channels/instagram/select?session=123", + platformOwnerId: "owner-1", + }) + mockCompleteAuthorization.mockResolvedValueOnce({ + id: "123", + workspaceId: "ws-1", + status: "awaiting_selection", + targets: [{ id: "ig-1", selectable: true }], + actorUserId: "user-1", + }) + + await handleCallback("messenger", buildRequest("123.abc-nonce")) + + expect(mockConnectTargets).not.toHaveBeenCalled() + expect(mockRedirect).toHaveBeenCalledWith( + "/channels/instagram/select?session=123", + ) + }) + + test("swallows an unexpected completeAuthorization error and still redirects to the completion page", async () => { + mockFindByNonce.mockResolvedValueOnce({ + id: "123", + provider: "messenger", + returnUrl: null, + platformOwnerId: "owner-1", + }) + mockCompleteAuthorization.mockRejectedValueOnce(new Error("boom")) + + await handleCallback("messenger", buildRequest("123.abc-nonce")) + + expect(mockRedirect).toHaveBeenCalledWith("/connect/123") + }) +}) diff --git a/apps/builder/__tests__/oauth-reconnect-callback.test.ts b/apps/builder/__tests__/oauth-reconnect-callback.test.ts index d2b1cc1371..4499d47c00 100644 --- a/apps/builder/__tests__/oauth-reconnect-callback.test.ts +++ b/apps/builder/__tests__/oauth-reconnect-callback.test.ts @@ -37,8 +37,6 @@ const { mockCreateGoogleFromOAuthCallback, mockResolveOwnerForWorkspace, mockGetCurrentUser, - mockEncryptAuth, - mockCookieSet, mockNotFound, mockRedirect, mockAuditRecord, @@ -78,8 +76,6 @@ const { mockCreateGoogleFromOAuthCallback: vi.fn(), mockResolveOwnerForWorkspace: vi.fn(async () => "platform-owner-1"), mockGetCurrentUser: vi.fn(), - mockEncryptAuth: vi.fn(async () => "encrypted-token"), - mockCookieSet: vi.fn(), mockNotFound: vi.fn(() => { throw new Error("not found") }), @@ -131,6 +127,21 @@ vi.mock("@chatbotx.io/database/client", () => ({ db: { transaction: vi.fn() }, })) +vi.mock("@chatbotx.io/business/connect-session", () => ({ + connectSessionService: { + findByNonce: vi.fn(), + fail: vi.fn(), + }, +})) + +vi.mock("@chatbotx.io/connections", () => ({ + connectionService: { + completeAuthorization: vi.fn(), + connectTargets: vi.fn(), + }, + CONNECTION_REGISTRY: {}, +})) + vi.mock("@chatbotx.io/database/schema", () => ({ integrationGoogleSheetsModel: {}, integrationModel: {}, @@ -179,10 +190,6 @@ vi.mock("@chatbotx.io/utils", async (importOriginal) => { } }) -vi.mock("next/headers", () => ({ - cookies: vi.fn(async () => ({ set: mockCookieSet })), -})) - vi.mock("next/navigation", () => ({ notFound: mockNotFound, redirect: mockRedirect, @@ -238,24 +245,6 @@ vi.mock("@/lib/log", () => ({ logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() }, })) -vi.mock("@/lib/facebook-pending-auth", async (importOriginal) => { - // The real `pendingAuthCookieOptions` — these tests assert the set site - // passes the helper's own value through, not a copy that could drift from - // it (the cookie's `path` is what makes the connect routes reachable). - const actual = - await importOriginal() - - return { - encryptAuth: mockEncryptAuth, - FB_INSTAGRAM_FACEBOOK_PENDING_AUTH_COOKIE: "igfb-pending-auth", - FB_INSTAGRAM_PENDING_AUTH_COOKIE: "ig-pending-auth", - FB_MESSENGER_PENDING_AUTH_COOKIE: "messenger-pending-auth", - FB_PENDING_AUTH_MAX_AGE: 600, - // The real writer: these tests assert what actually reaches the cookie - // store, including the expiry of the picker-scoped predecessor. - writePendingAuth: actual.writePendingAuth, - } -}) vi.mock("@/lib/oauth-broker", () => ({ buildBrokerCallbackUrl: (path: string) => `https://broker.example.com${path}`, getBrokerOrigin: () => "https://broker.example.com", @@ -339,41 +328,13 @@ describe("handleCallback OAuth reconnect", () => { callbackUrl: "https://broker.example.com/integrations/messenger/callback", }) expect(mockExchangeMessengerCode).not.toHaveBeenCalled() - expect(mockCookieSet).not.toHaveBeenCalled() const redirectTarget = new URL(mockRedirect.mock.calls[0][0]) expect(redirectTarget.searchParams.get("reconnect")).toBe("success") expect(redirectTarget.searchParams.get("channel")).toBe("messenger") }) - test("first-channel workspace creation hitting the plan limit redirects to /channels/create?error=… instead of throwing", async () => { - const { workspaceLimitReachedException } = await import( - "@chatbotx.io/business/errors" - ) - const { workspaceService } = await import("@chatbotx.io/business") - vi.mocked(workspaceService.create).mockRejectedValueOnce( - workspaceLimitReachedException(), - ) - // Next's real `redirect` throws; mirror that here so the handler stops - // where production would, then restore the shared no-op for later tests. - mockRedirect.mockImplementation((path: string) => { - throw new Error(`redirect:${path}`) - }) - try { - await expect( - handleCallback( - "messenger", - buildCallbackRequest("messenger", { referer: REFERER }), - ), - ).rejects.toThrow("redirect:/channels/create?error=workspaceLimitReached") - } finally { - mockRedirect.mockImplementation(() => undefined) - } - - expect(mockExchangeMessengerCode).not.toHaveBeenCalled() - expect(mockCookieSet).not.toHaveBeenCalled() - }) - + // Plain legacy connect callbacks are superseded by the ConnectSession flow. test("messenger reconnect failure redirects with the error reason", async () => { mockReconnectMessengerHandler.mockResolvedValue({ status: "error", @@ -461,39 +422,6 @@ describe("handleCallback OAuth reconnect", () => { expect(mockRedirect).not.toHaveBeenCalled() }) - test("connect flow without reconnect state still runs the page-select flow", async () => { - mockExchangeMessengerCode.mockResolvedValue("short-token") - mockExchangeMessengerLongLivedToken.mockResolvedValue("long-token") - - await handleCallback( - "messenger", - buildCallbackRequest("messenger", { - workspaceId: "1", - referer: REFERER, - }), - ) - - expect(mockReconnectMessengerHandler).not.toHaveBeenCalled() - expect(mockEncryptAuth).toHaveBeenCalledWith( - expect.objectContaining({ - userToken: "long-token", - userId: "fb-user-1", - userName: "FB User", - userAvatarUrl: "https://fb.example/avatar.jpg", - }), - ) - expect(mockCookieSet).toHaveBeenCalledWith( - "messenger-pending-auth", - "encrypted-token", - // Scoped for the connect route, not the picker page — see - // `pendingAuthCookieOptions`. - expect.objectContaining({ path: "/", httpOnly: true, sameSite: "lax" }), - ) - expect(mockRedirect).toHaveBeenCalledWith( - new URL("/channels/messenger/select", REFERER).toString(), - ) - }) - test("google calendar callback resolves credentials with the tenant-aware owner", async () => { mockExchangeAndVerifyGoogleCalendar.mockResolvedValue({ auth: { type: "oauth2", tokens: { accessToken: "google-token" } }, @@ -806,7 +734,6 @@ describe("handleCallback OAuth reconnect", () => { integrationId: "7", userToken: "ig-user-token", }) - expect(mockCookieSet).not.toHaveBeenCalled() const redirectTarget = new URL(mockRedirect.mock.calls[0][0]) expect(redirectTarget.searchParams.get("reconnect")).toBe("success") diff --git a/apps/builder/__tests__/resolve-connect-session.test.ts b/apps/builder/__tests__/resolve-connect-session.test.ts index cfd4fe7995..2e156d2b06 100644 --- a/apps/builder/__tests__/resolve-connect-session.test.ts +++ b/apps/builder/__tests__/resolve-connect-session.test.ts @@ -4,39 +4,33 @@ import { ChatbotXException } from "@chatbotx.io/business/errors" import { beforeEach, describe, expect, test, vi } from "vitest" // --------------------------------------------------------------------------- -// `resolveConnectSession` is the shared plan §2.4 steps 1-4 helper every -// per-account connect action (Messenger today; Instagram's two actions in a -// later phase) delegates to instead of re-implementing the same five checks: -// pending-auth cookie -> workspace + membership -> owner quota/trial gate -> -// platform credential + branding menu entry. Every failure throws one of the -// session-level exceptions; this file pins each branch plus the happy path's -// full return shape. +// `resolveConnectSession` is the shared per-account connect helper every +// picker action (Messenger, Instagram direct, Instagram-via-Facebook) +// delegates to instead of re-implementing the same checks: `ConnectSession` +// row lookup -> workspace + membership -> owner quota/trial gate -> +// platform credential + branding menu entry. Every failure throws one of +// the session-level exceptions; this file pins each branch plus the happy +// path's full return shape. // --------------------------------------------------------------------------- const { checkWorkspaceOwnerAccessMock, + findByIdMock, findWorkspaceMock, isMemberMock, platformCredentialResolveMock, - readPendingAuthMock, - resolvePlatformOwnerIdMock, resolveTenantSettingsMock, } = vi.hoisted(() => ({ checkWorkspaceOwnerAccessMock: vi.fn(), + findByIdMock: vi.fn(), findWorkspaceMock: vi.fn(), isMemberMock: vi.fn(), platformCredentialResolveMock: vi.fn(), - readPendingAuthMock: vi.fn(), - resolvePlatformOwnerIdMock: vi.fn(), resolveTenantSettingsMock: vi.fn(), })) -vi.mock("@/lib/facebook-pending-auth", () => ({ - readPendingAuth: readPendingAuthMock, -})) - -vi.mock("@/lib/platform-credential-owner", () => ({ - resolvePlatformOwnerId: resolvePlatformOwnerIdMock, +vi.mock("@chatbotx.io/business/connect-session", () => ({ + connectSessionService: { findById: findByIdMock }, })) // Fully replaced (not `importOriginal`) — the real module's @@ -75,36 +69,36 @@ const { resolveConnectSession } = await import( "@/features/channel-connect/lib/resolve-connect-session" ) -const pendingAuth = { - userToken: "user-token-1", +const session = { + id: "session-1", workspaceId: "ws-1", - referer: "/channels/create", - version: "v23.0", - expiresAt: Date.now() + 600_000, + platformOwnerId: "owner-1", + provider: "messenger", + status: "awaiting_selection", + targets: [], } describe("resolveConnectSession", () => { beforeEach(() => { vi.clearAllMocks() - readPendingAuthMock.mockResolvedValue(pendingAuth) + findByIdMock.mockResolvedValue(session) findWorkspaceMock.mockResolvedValue({ id: "ws-1", ownerId: "owner-1" }) isMemberMock.mockResolvedValue(true) checkWorkspaceOwnerAccessMock.mockResolvedValue(null) - resolvePlatformOwnerIdMock.mockResolvedValue("owner-1") platformCredentialResolveMock.mockResolvedValue({ config: { clientId: "client-1", clientSecret: "secret-1" }, }) resolveTenantSettingsMock.mockResolvedValue({ appUrl: "https://app.test" }) }) - test("throws connectSessionExpired when the pending-auth cookie is missing/invalid", async () => { - readPendingAuthMock.mockResolvedValue(null) + test("throws connectSessionExpired when the session is missing/expired", async () => { + findByIdMock.mockResolvedValue(null) await expect( resolveConnectSession({ userId: "user-1", - cookieName: "fb_messenger_pending_auth", + sessionId: "session-1", credentialType: "messenger", brandingChannel: "messenger", }), @@ -118,7 +112,7 @@ describe("resolveConnectSession", () => { await expect( resolveConnectSession({ userId: "user-1", - cookieName: "fb_messenger_pending_auth", + sessionId: "session-1", credentialType: "messenger", brandingChannel: "messenger", }), @@ -131,9 +125,6 @@ describe("resolveConnectSession", () => { checkWorkspaceOwnerAccessMock.mockRejectedValue( new Error("must not be called"), ) - resolvePlatformOwnerIdMock.mockRejectedValue( - new Error("must not be called"), - ) platformCredentialResolveMock.mockRejectedValue( new Error("must not be called"), ) @@ -141,13 +132,12 @@ describe("resolveConnectSession", () => { await expect( resolveConnectSession({ userId: "user-1", - cookieName: "fb_messenger_pending_auth", + sessionId: "session-1", credentialType: "messenger", brandingChannel: "messenger", }), ).rejects.toMatchObject({ code: "notWorkspaceMember" }) expect(checkWorkspaceOwnerAccessMock).not.toHaveBeenCalled() - expect(resolvePlatformOwnerIdMock).not.toHaveBeenCalled() expect(platformCredentialResolveMock).not.toHaveBeenCalled() }) @@ -157,12 +147,12 @@ describe("resolveConnectSession", () => { await expect( resolveConnectSession({ userId: "user-1", - cookieName: "fb_messenger_pending_auth", + sessionId: "session-1", credentialType: "messenger", brandingChannel: "messenger", }), ).rejects.toMatchObject({ code: "trialExpired" }) - expect(resolvePlatformOwnerIdMock).not.toHaveBeenCalled() + expect(platformCredentialResolveMock).not.toHaveBeenCalled() }) test("throws macLimitReached when the workspace owner is blocked on MAC", async () => { @@ -171,20 +161,34 @@ describe("resolveConnectSession", () => { await expect( resolveConnectSession({ userId: "user-1", - cookieName: "fb_messenger_pending_auth", + sessionId: "session-1", credentialType: "messenger", brandingChannel: "messenger", }), ).rejects.toMatchObject({ code: "macLimitReached" }) }) + test("throws credentialMissing when the session has no platform owner", async () => { + findByIdMock.mockResolvedValue({ ...session, platformOwnerId: null }) + + await expect( + resolveConnectSession({ + userId: "user-1", + sessionId: "session-1", + credentialType: "messenger", + brandingChannel: "messenger", + }), + ).rejects.toMatchObject({ code: "credentialMissing" }) + expect(platformCredentialResolveMock).not.toHaveBeenCalled() + }) + test("throws credentialMissing when the owner has no configured credential", async () => { platformCredentialResolveMock.mockResolvedValue(undefined) await expect( resolveConnectSession({ userId: "user-1", - cookieName: "fb_messenger_pending_auth", + sessionId: "session-1", credentialType: "messenger", brandingChannel: "messenger", }), @@ -195,13 +199,13 @@ describe("resolveConnectSession", () => { test("happy path resolves every field, sourcing the branding channel from the caller (not a hard-coded literal)", async () => { const result = await resolveConnectSession({ userId: "user-1", - cookieName: "fb_messenger_pending_auth", + sessionId: "session-1", credentialType: "messenger", brandingChannel: "instagram", }) expect(result).toEqual({ - pendingAuth, + session, workspace: { id: "ws-1", ownerId: "owner-1" }, platformOwnerId: "owner-1", credential: { @@ -218,9 +222,8 @@ describe("resolveConnectSession", () => { ownerId: "owner-1", type: "messenger", }) - expect(resolvePlatformOwnerIdMock).toHaveBeenCalledWith({ - userId: "user-1", - workspaceId: "ws-1", + expect(findWorkspaceMock).toHaveBeenCalledWith({ + where: { id: "ws-1" }, }) }) }) diff --git a/apps/builder/__tests__/select-facebook-accounts.test.tsx b/apps/builder/__tests__/select-facebook-accounts.test.tsx index 8949ba558e..6e57b0de27 100644 --- a/apps/builder/__tests__/select-facebook-accounts.test.tsx +++ b/apps/builder/__tests__/select-facebook-accounts.test.tsx @@ -102,7 +102,13 @@ describe("SelectFacebookAccounts", () => { function renderAccounts(items: ConnectPickerItem[]) { act(() => { - root.render() + root.render( + , + ) }) } @@ -138,7 +144,7 @@ describe("SelectFacebookAccounts", () => { ) }) - test("submitting a single selected item posts only { igId } to the instagram-via-facebook connect route — no token, no workspaceId", async () => { + test("submitting a single selected item posts only { sessionId, igId } to the instagram-via-facebook connect route — no token, no workspaceId", async () => { mockConnectViaApi.mockResolvedValue({ kind: "outcome", outcome: { @@ -164,16 +170,16 @@ describe("SelectFacebookAccounts", () => { }) expect(mockConnectViaApi).toHaveBeenCalledTimes(1) - // Ids only, and the channel's own route — no token, no workspaceId. + // Session and account IDs only, and the channel's own route — no token or workspaceId. expect(mockConnectViaApi.mock.calls[0]?.[0]).toMatchObject({ // The registry entry itself — it now carries the typed oRPC procedure, // so identity is what pins the channel, not a URL string. route: CONNECT_CHANNEL_REGISTRY.instagram.connectRoute, - body: { igId: "ig-selectable" }, + body: { sessionId: "session-1", igId: "ig-selectable" }, }) expect( Object.keys(mockConnectViaApi.mock.calls[0]?.[0]?.body ?? {}), - ).toEqual(["igId"]) + ).toEqual(["sessionId", "igId"]) await act(async () => { await Promise.resolve() diff --git a/apps/builder/__tests__/smtp-actions-thin.test.ts b/apps/builder/__tests__/smtp-actions-thin.test.ts index 867e440f40..0589bebd1c 100644 --- a/apps/builder/__tests__/smtp-actions-thin.test.ts +++ b/apps/builder/__tests__/smtp-actions-thin.test.ts @@ -22,6 +22,7 @@ vi.mock("@/lib/safe-action", () => { chain.action = (fn: unknown) => fn return { workspaceActionClient: chain, + workspaceActionClientAllowExpired: chain, } }) diff --git a/apps/builder/__tests__/smtp-integrations-public-api.test.ts b/apps/builder/__tests__/smtp-integrations-public-api.test.ts index 9e0d63b4e9..4aa9341d91 100644 --- a/apps/builder/__tests__/smtp-integrations-public-api.test.ts +++ b/apps/builder/__tests__/smtp-integrations-public-api.test.ts @@ -93,8 +93,8 @@ beforeEach(() => { callOrder.length = 0 }) -test("registers the smtp integrations public router under the channels scope", () => { - expect(scopeArgAtImport).toBe("channels") +test("registers the smtp integrations public router under the connections scope", () => { + expect(scopeArgAtImport).toBe("connections") }) describe("POST /v1/smtp-integrations", () => { diff --git a/apps/builder/__tests__/user-persistent-menus-public-api.test.ts b/apps/builder/__tests__/user-persistent-menus-public-api.test.ts index 0e189fb434..26732ace86 100644 --- a/apps/builder/__tests__/user-persistent-menus-public-api.test.ts +++ b/apps/builder/__tests__/user-persistent-menus-public-api.test.ts @@ -95,8 +95,8 @@ beforeEach(() => { vi.clearAllMocks() }) -test("registers the user persistent menus public router under the channels scope", () => { - expect(scopeArgAtImport).toBe("channels") +test("registers the user persistent menus public router under the connections scope", () => { + expect(scopeArgAtImport).toBe("connections") }) describe("GET /v1/user-persistent-menus", () => { diff --git a/apps/builder/__tests__/webchats-public-api.test.ts b/apps/builder/__tests__/webchats-public-api.test.ts index 9cce3b04ed..fb9931c430 100644 --- a/apps/builder/__tests__/webchats-public-api.test.ts +++ b/apps/builder/__tests__/webchats-public-api.test.ts @@ -115,8 +115,8 @@ beforeEach(() => { isCommunity.mockReturnValue(false) }) -test("registers the webchats public router under the channels scope", () => { - expect(scopeArgAtImport).toBe("channels") +test("registers the webchats public router under the connections scope", () => { + expect(scopeArgAtImport).toBe("connections") }) test("webchatPublicResource never exposes workspaceId or the auth credential blob", () => { diff --git a/apps/builder/__tests__/webhooks-public-api.test.ts b/apps/builder/__tests__/webhooks-public-api.test.ts index f26c8f5e39..0e186496b3 100644 --- a/apps/builder/__tests__/webhooks-public-api.test.ts +++ b/apps/builder/__tests__/webhooks-public-api.test.ts @@ -79,7 +79,7 @@ const findProcedure = (method: string, path: string) => { // Captured before the first beforeEach's clearAllMocks() erases the // import-time call record — the router calls -// workspaceTokenAuthAPIForScope("integrations") exactly once, at module load +// workspaceTokenAuthAPIForScope("connections") exactly once, at module load // (line 59 above), never again during the test run. const scopeArgAtImport = workspaceTokenAuthAPIForScope.mock.calls[0]?.[0] @@ -87,8 +87,8 @@ beforeEach(() => { vi.clearAllMocks() }) -test("registers the webhooks public router under the integrations scope", () => { - expect(scopeArgAtImport).toBe("integrations") +test("registers the webhooks public router under the connections scope", () => { + expect(scopeArgAtImport).toBe("connections") }) describe("GET /v1/webhooks", () => { diff --git a/apps/builder/__tests__/workspace-token-scope-registry.test.ts b/apps/builder/__tests__/workspace-token-scope-registry.test.ts index c11160f284..41c8c02a79 100644 --- a/apps/builder/__tests__/workspace-token-scope-registry.test.ts +++ b/apps/builder/__tests__/workspace-token-scope-registry.test.ts @@ -6,7 +6,7 @@ import { } from "../src/features/workspaces/lib/workspace-token-scopes" const NEW_SCOPES = [ - "channels", + "connections", "minigames", "appointments", "media", @@ -14,17 +14,19 @@ const NEW_SCOPES = [ ] as const describe("workspaceApiTokenScopes", () => { - test("includes the 5 newly named resource-area scopes", () => { + test("includes the newly named resource-area scopes, and no longer the merged 'channels'/'integrations' scopes", () => { for (const scope of NEW_SCOPES) { expect(workspaceApiTokenScopes.options).toContain(scope) } - expect(workspaceApiTokenScopes.options).toHaveLength(12) + expect(workspaceApiTokenScopes.options).not.toContain("channels") + expect(workspaceApiTokenScopes.options).not.toContain("integrations") + expect(workspaceApiTokenScopes.options).toHaveLength(11) }) }) describe("orderedWorkspaceApiTokenScopes", () => { - test("has 12 entries with unique, contiguous orders", () => { - expect(orderedWorkspaceApiTokenScopes).toHaveLength(12) + test("has 11 entries with unique, contiguous orders", () => { + expect(orderedWorkspaceApiTokenScopes).toHaveLength(11) const orders = orderedWorkspaceApiTokenScopes .map((scope) => workspaceApiTokenScopeRegistry[scope].order) diff --git a/apps/builder/messages/ar.json b/apps/builder/messages/ar.json index 7525ec609b..d642df646b 100644 --- a/apps/builder/messages/ar.json +++ b/apps/builder/messages/ar.json @@ -2304,8 +2304,7 @@ "broadcasts": "البث", "analytics": "التحليلات", "ecommerce": "التجارة الإلكترونية", - "integrations": "التكاملات", - "channels": "القنوات", + "connections": "الاتصالات", "minigames": "الألعاب المصغرة", "appointments": "المواعيد", "media": "الوسائط", @@ -6089,5 +6088,19 @@ }, "accountFields": { "title": "حقول البوت" + }, + "connectSessionPage": { + "processingTitle": "Finishing up…", + "processingDescription": "You can close this window. The application that started this connection will finish setting things up shortly.", + "completedTitle": "Connected!", + "completedDescription": "You can close this window now.", + "failedTitle": "Connection failed.", + "failedDescription": "Something went wrong while connecting. Please try again from the application that started this.", + "cancelledTitle": "Connection cancelled.", + "cancelledDescription": "You can close this window.", + "expiredTitle": "This link has expired.", + "expiredDescription": "Please start the connection again from the application that started this.", + "invalidTitle": "This link is no longer valid.", + "invalidDescription": "This connection link is invalid or has already been used." } } diff --git a/apps/builder/messages/da.json b/apps/builder/messages/da.json index 503b7cbc7e..11af257ddb 100644 --- a/apps/builder/messages/da.json +++ b/apps/builder/messages/da.json @@ -2167,8 +2167,7 @@ "broadcasts": "Udsendelser", "analytics": "Analyse", "ecommerce": "E-handel", - "integrations": "Integrationer", - "channels": "Kanaler", + "connections": "Forbindelser", "minigames": "Minispil", "appointments": "Aftaler", "media": "Medier", @@ -6089,5 +6088,19 @@ }, "accountFields": { "title": "Bot Felter" + }, + "connectSessionPage": { + "processingTitle": "Finishing up…", + "processingDescription": "You can close this window. The application that started this connection will finish setting things up shortly.", + "completedTitle": "Connected!", + "completedDescription": "You can close this window now.", + "failedTitle": "Connection failed.", + "failedDescription": "Something went wrong while connecting. Please try again from the application that started this.", + "cancelledTitle": "Connection cancelled.", + "cancelledDescription": "You can close this window.", + "expiredTitle": "This link has expired.", + "expiredDescription": "Please start the connection again from the application that started this.", + "invalidTitle": "This link is no longer valid.", + "invalidDescription": "This connection link is invalid or has already been used." } } diff --git a/apps/builder/messages/de.json b/apps/builder/messages/de.json index a35b1d092e..3f3555f9cb 100644 --- a/apps/builder/messages/de.json +++ b/apps/builder/messages/de.json @@ -2167,8 +2167,7 @@ "broadcasts": "Broadcasts", "analytics": "Analysen", "ecommerce": "E-Commerce", - "integrations": "Integrationen", - "channels": "Kanäle", + "connections": "Verbindungen", "minigames": "Minispiele", "appointments": "Termine", "media": "Medien", @@ -6089,5 +6088,19 @@ }, "accountFields": { "title": "Bot-Felder" + }, + "connectSessionPage": { + "processingTitle": "Finishing up…", + "processingDescription": "You can close this window. The application that started this connection will finish setting things up shortly.", + "completedTitle": "Connected!", + "completedDescription": "You can close this window now.", + "failedTitle": "Connection failed.", + "failedDescription": "Something went wrong while connecting. Please try again from the application that started this.", + "cancelledTitle": "Connection cancelled.", + "cancelledDescription": "You can close this window.", + "expiredTitle": "This link has expired.", + "expiredDescription": "Please start the connection again from the application that started this.", + "invalidTitle": "This link is no longer valid.", + "invalidDescription": "This connection link is invalid or has already been used." } } diff --git a/apps/builder/messages/en.json b/apps/builder/messages/en.json index b8b950e9c9..b19800052f 100644 --- a/apps/builder/messages/en.json +++ b/apps/builder/messages/en.json @@ -2304,8 +2304,7 @@ "broadcasts": "Broadcasts", "analytics": "Analytics", "ecommerce": "Ecommerce", - "integrations": "Integrations", - "channels": "Channels", + "connections": "Connections", "minigames": "Minigames", "appointments": "Appointments", "media": "Media", @@ -5184,6 +5183,20 @@ "unavailableTitle": "Unsubscribe unavailable.", "unavailableDescription": "This workspace is no longer available." }, + "connectSessionPage": { + "processingTitle": "Finishing up…", + "processingDescription": "You can close this window. The application that started this connection will finish setting things up shortly.", + "completedTitle": "Connected!", + "completedDescription": "You can close this window now.", + "failedTitle": "Connection failed.", + "failedDescription": "Something went wrong while connecting. Please try again from the application that started this.", + "cancelledTitle": "Connection cancelled.", + "cancelledDescription": "You can close this window.", + "expiredTitle": "This link has expired.", + "expiredDescription": "Please start the connection again from the application that started this.", + "invalidTitle": "This link is no longer valid.", + "invalidDescription": "This connection link is invalid or has already been used." + }, "templatesPublicPage": { "invalidTitle": "This link is no longer available.", "invalidDescription": "This template share link is invalid, disabled, or has expired.", diff --git a/apps/builder/messages/es.json b/apps/builder/messages/es.json index 72604ffa44..635f8a3a1d 100644 --- a/apps/builder/messages/es.json +++ b/apps/builder/messages/es.json @@ -2304,8 +2304,7 @@ "broadcasts": "Difusiones", "analytics": "Analítica", "ecommerce": "Comercio electrónico", - "integrations": "Integraciones", - "channels": "Canales", + "connections": "Conexiones", "minigames": "Minijuegos", "appointments": "Citas", "media": "Medios", @@ -6089,5 +6088,19 @@ }, "accountFields": { "title": "Campos del bot" + }, + "connectSessionPage": { + "processingTitle": "Finishing up…", + "processingDescription": "You can close this window. The application that started this connection will finish setting things up shortly.", + "completedTitle": "Connected!", + "completedDescription": "You can close this window now.", + "failedTitle": "Connection failed.", + "failedDescription": "Something went wrong while connecting. Please try again from the application that started this.", + "cancelledTitle": "Connection cancelled.", + "cancelledDescription": "You can close this window.", + "expiredTitle": "This link has expired.", + "expiredDescription": "Please start the connection again from the application that started this.", + "invalidTitle": "This link is no longer valid.", + "invalidDescription": "This connection link is invalid or has already been used." } } diff --git a/apps/builder/messages/fi.json b/apps/builder/messages/fi.json index d74097fc9e..18dc0c3485 100644 --- a/apps/builder/messages/fi.json +++ b/apps/builder/messages/fi.json @@ -2167,8 +2167,7 @@ "broadcasts": "Joukkoviestit", "analytics": "Analytiikka", "ecommerce": "Verkkokauppa", - "integrations": "Integraatiot", - "channels": "Kanavat", + "connections": "Yhteydet", "minigames": "Minipelit", "appointments": "Tapaamiset", "media": "Media", @@ -6089,5 +6088,19 @@ }, "accountFields": { "title": "Bottikentät" + }, + "connectSessionPage": { + "processingTitle": "Finishing up…", + "processingDescription": "You can close this window. The application that started this connection will finish setting things up shortly.", + "completedTitle": "Connected!", + "completedDescription": "You can close this window now.", + "failedTitle": "Connection failed.", + "failedDescription": "Something went wrong while connecting. Please try again from the application that started this.", + "cancelledTitle": "Connection cancelled.", + "cancelledDescription": "You can close this window.", + "expiredTitle": "This link has expired.", + "expiredDescription": "Please start the connection again from the application that started this.", + "invalidTitle": "This link is no longer valid.", + "invalidDescription": "This connection link is invalid or has already been used." } } diff --git a/apps/builder/messages/fr.json b/apps/builder/messages/fr.json index a66bcfeddf..cc32934f6f 100644 --- a/apps/builder/messages/fr.json +++ b/apps/builder/messages/fr.json @@ -2167,8 +2167,7 @@ "broadcasts": "Diffusions", "analytics": "Analytique", "ecommerce": "E-commerce", - "integrations": "Intégrations", - "channels": "Canaux", + "connections": "Connexions", "minigames": "Mini-jeux", "appointments": "Rendez-vous", "media": "Médias", @@ -6089,5 +6088,19 @@ }, "accountFields": { "title": "Champs du bot" + }, + "connectSessionPage": { + "processingTitle": "Finishing up…", + "processingDescription": "You can close this window. The application that started this connection will finish setting things up shortly.", + "completedTitle": "Connected!", + "completedDescription": "You can close this window now.", + "failedTitle": "Connection failed.", + "failedDescription": "Something went wrong while connecting. Please try again from the application that started this.", + "cancelledTitle": "Connection cancelled.", + "cancelledDescription": "You can close this window.", + "expiredTitle": "This link has expired.", + "expiredDescription": "Please start the connection again from the application that started this.", + "invalidTitle": "This link is no longer valid.", + "invalidDescription": "This connection link is invalid or has already been used." } } diff --git a/apps/builder/messages/he.json b/apps/builder/messages/he.json index 95a92eb866..51c6cf3a16 100644 --- a/apps/builder/messages/he.json +++ b/apps/builder/messages/he.json @@ -4823,8 +4823,7 @@ "broadcasts": "שידורים", "analytics": "אנליטיקה", "ecommerce": "מסחר אלקטרוני", - "integrations": "אינטגרציות", - "channels": "ערוצים", + "connections": "חיבורים", "minigames": "משחונים", "appointments": "פגישות", "media": "מדיה", @@ -6089,5 +6088,19 @@ }, "accountFields": { "title": "שדות בוט" + }, + "connectSessionPage": { + "processingTitle": "Finishing up…", + "processingDescription": "You can close this window. The application that started this connection will finish setting things up shortly.", + "completedTitle": "Connected!", + "completedDescription": "You can close this window now.", + "failedTitle": "Connection failed.", + "failedDescription": "Something went wrong while connecting. Please try again from the application that started this.", + "cancelledTitle": "Connection cancelled.", + "cancelledDescription": "You can close this window.", + "expiredTitle": "This link has expired.", + "expiredDescription": "Please start the connection again from the application that started this.", + "invalidTitle": "This link is no longer valid.", + "invalidDescription": "This connection link is invalid or has already been used." } } diff --git a/apps/builder/messages/id.json b/apps/builder/messages/id.json index e73a2f9714..d48b9359f5 100644 --- a/apps/builder/messages/id.json +++ b/apps/builder/messages/id.json @@ -2167,8 +2167,7 @@ "broadcasts": "Siaran", "analytics": "Analitik", "ecommerce": "E-commerce", - "integrations": "Integrasi", - "channels": "Saluran", + "connections": "Koneksi", "minigames": "Minigame", "appointments": "Janji Temu", "media": "Media", @@ -6089,5 +6088,19 @@ }, "accountFields": { "title": "Kolom Bot" + }, + "connectSessionPage": { + "processingTitle": "Finishing up…", + "processingDescription": "You can close this window. The application that started this connection will finish setting things up shortly.", + "completedTitle": "Connected!", + "completedDescription": "You can close this window now.", + "failedTitle": "Connection failed.", + "failedDescription": "Something went wrong while connecting. Please try again from the application that started this.", + "cancelledTitle": "Connection cancelled.", + "cancelledDescription": "You can close this window.", + "expiredTitle": "This link has expired.", + "expiredDescription": "Please start the connection again from the application that started this.", + "invalidTitle": "This link is no longer valid.", + "invalidDescription": "This connection link is invalid or has already been used." } } diff --git a/apps/builder/messages/it.json b/apps/builder/messages/it.json index 7a48bdc14d..9d9248e5c3 100644 --- a/apps/builder/messages/it.json +++ b/apps/builder/messages/it.json @@ -1440,8 +1440,7 @@ "broadcasts": "Trasmissioni", "analytics": "Analisi", "ecommerce": "E-commerce", - "integrations": "Integrazioni", - "channels": "Canali", + "connections": "Connessioni", "minigames": "Minigiochi", "appointments": "Appuntamenti", "media": "Media", @@ -6089,5 +6088,19 @@ }, "accountFields": { "title": "Campi del bot" + }, + "connectSessionPage": { + "processingTitle": "Finishing up…", + "processingDescription": "You can close this window. The application that started this connection will finish setting things up shortly.", + "completedTitle": "Connected!", + "completedDescription": "You can close this window now.", + "failedTitle": "Connection failed.", + "failedDescription": "Something went wrong while connecting. Please try again from the application that started this.", + "cancelledTitle": "Connection cancelled.", + "cancelledDescription": "You can close this window.", + "expiredTitle": "This link has expired.", + "expiredDescription": "Please start the connection again from the application that started this.", + "invalidTitle": "This link is no longer valid.", + "invalidDescription": "This connection link is invalid or has already been used." } } diff --git a/apps/builder/messages/ja.json b/apps/builder/messages/ja.json index 1649dfc53f..ac22c69daf 100644 --- a/apps/builder/messages/ja.json +++ b/apps/builder/messages/ja.json @@ -2167,8 +2167,7 @@ "broadcasts": "配信", "analytics": "アナリティクス", "ecommerce": "eコマース", - "integrations": "連携", - "channels": "チャネル", + "connections": "接続", "minigames": "ミニゲーム", "appointments": "予約", "media": "メディア", @@ -6089,5 +6088,19 @@ }, "accountFields": { "title": "ボットフィールド" + }, + "connectSessionPage": { + "processingTitle": "Finishing up…", + "processingDescription": "You can close this window. The application that started this connection will finish setting things up shortly.", + "completedTitle": "Connected!", + "completedDescription": "You can close this window now.", + "failedTitle": "Connection failed.", + "failedDescription": "Something went wrong while connecting. Please try again from the application that started this.", + "cancelledTitle": "Connection cancelled.", + "cancelledDescription": "You can close this window.", + "expiredTitle": "This link has expired.", + "expiredDescription": "Please start the connection again from the application that started this.", + "invalidTitle": "This link is no longer valid.", + "invalidDescription": "This connection link is invalid or has already been used." } } diff --git a/apps/builder/messages/nl.json b/apps/builder/messages/nl.json index e3003401cf..23e3e9adc4 100644 --- a/apps/builder/messages/nl.json +++ b/apps/builder/messages/nl.json @@ -1980,8 +1980,7 @@ "broadcasts": "Uitzendingen", "analytics": "Analyse", "ecommerce": "E-commerce", - "integrations": "Integraties", - "channels": "Kanalen", + "connections": "Verbindingen", "minigames": "Minigames", "appointments": "Afspraken", "media": "Media", @@ -6089,5 +6088,19 @@ }, "accountFields": { "title": "Botvelden" + }, + "connectSessionPage": { + "processingTitle": "Finishing up…", + "processingDescription": "You can close this window. The application that started this connection will finish setting things up shortly.", + "completedTitle": "Connected!", + "completedDescription": "You can close this window now.", + "failedTitle": "Connection failed.", + "failedDescription": "Something went wrong while connecting. Please try again from the application that started this.", + "cancelledTitle": "Connection cancelled.", + "cancelledDescription": "You can close this window.", + "expiredTitle": "This link has expired.", + "expiredDescription": "Please start the connection again from the application that started this.", + "invalidTitle": "This link is no longer valid.", + "invalidDescription": "This connection link is invalid or has already been used." } } diff --git a/apps/builder/messages/pt-BR.json b/apps/builder/messages/pt-BR.json index 3e31d80fb4..00bce1b817 100644 --- a/apps/builder/messages/pt-BR.json +++ b/apps/builder/messages/pt-BR.json @@ -2167,8 +2167,7 @@ "broadcasts": "Transmissões", "analytics": "Análises", "ecommerce": "E-commerce", - "integrations": "Integrações", - "channels": "Canais", + "connections": "Conexões", "minigames": "Minijogos", "appointments": "Agendamentos", "media": "Mídia", @@ -6089,5 +6088,19 @@ }, "accountFields": { "title": "Campos do bot" + }, + "connectSessionPage": { + "processingTitle": "Finishing up…", + "processingDescription": "You can close this window. The application that started this connection will finish setting things up shortly.", + "completedTitle": "Connected!", + "completedDescription": "You can close this window now.", + "failedTitle": "Connection failed.", + "failedDescription": "Something went wrong while connecting. Please try again from the application that started this.", + "cancelledTitle": "Connection cancelled.", + "cancelledDescription": "You can close this window.", + "expiredTitle": "This link has expired.", + "expiredDescription": "Please start the connection again from the application that started this.", + "invalidTitle": "This link is no longer valid.", + "invalidDescription": "This connection link is invalid or has already been used." } } diff --git a/apps/builder/messages/pt-PT.json b/apps/builder/messages/pt-PT.json index 82b510891e..afd3105b92 100644 --- a/apps/builder/messages/pt-PT.json +++ b/apps/builder/messages/pt-PT.json @@ -2167,8 +2167,7 @@ "broadcasts": "Difusões", "analytics": "Análises", "ecommerce": "Comércio eletrónico", - "integrations": "Integrações", - "channels": "Canais", + "connections": "Ligações", "minigames": "Minijogos", "appointments": "Marcações", "media": "Multimédia", @@ -6089,5 +6088,19 @@ }, "accountFields": { "title": "Campos do bot" + }, + "connectSessionPage": { + "processingTitle": "Finishing up…", + "processingDescription": "You can close this window. The application that started this connection will finish setting things up shortly.", + "completedTitle": "Connected!", + "completedDescription": "You can close this window now.", + "failedTitle": "Connection failed.", + "failedDescription": "Something went wrong while connecting. Please try again from the application that started this.", + "cancelledTitle": "Connection cancelled.", + "cancelledDescription": "You can close this window.", + "expiredTitle": "This link has expired.", + "expiredDescription": "Please start the connection again from the application that started this.", + "invalidTitle": "This link is no longer valid.", + "invalidDescription": "This connection link is invalid or has already been used." } } diff --git a/apps/builder/messages/ro.json b/apps/builder/messages/ro.json index 1d97e02ed6..61f8e285fa 100644 --- a/apps/builder/messages/ro.json +++ b/apps/builder/messages/ro.json @@ -2951,8 +2951,7 @@ "broadcasts": "Difuzări", "analytics": "Analitice", "ecommerce": "Comerț electronic", - "integrations": "Integrări", - "channels": "Canale", + "connections": "Conexiuni", "minigames": "Minijocuri", "appointments": "Programări", "media": "Media", @@ -6089,5 +6088,19 @@ }, "accountFields": { "title": "Câmpurile botului" + }, + "connectSessionPage": { + "processingTitle": "Finishing up…", + "processingDescription": "You can close this window. The application that started this connection will finish setting things up shortly.", + "completedTitle": "Connected!", + "completedDescription": "You can close this window now.", + "failedTitle": "Connection failed.", + "failedDescription": "Something went wrong while connecting. Please try again from the application that started this.", + "cancelledTitle": "Connection cancelled.", + "cancelledDescription": "You can close this window.", + "expiredTitle": "This link has expired.", + "expiredDescription": "Please start the connection again from the application that started this.", + "invalidTitle": "This link is no longer valid.", + "invalidDescription": "This connection link is invalid or has already been used." } } diff --git a/apps/builder/messages/sv.json b/apps/builder/messages/sv.json index fc2acd3eb6..d2397ee5b2 100644 --- a/apps/builder/messages/sv.json +++ b/apps/builder/messages/sv.json @@ -3002,8 +3002,7 @@ "broadcasts": "Utskick", "analytics": "Analys", "ecommerce": "E-handel", - "integrations": "Integrationer", - "channels": "Kanaler", + "connections": "Anslutningar", "minigames": "Minispel", "appointments": "Bokningar", "media": "Media", @@ -6089,5 +6088,19 @@ }, "accountFields": { "title": "Botfält" + }, + "connectSessionPage": { + "processingTitle": "Finishing up…", + "processingDescription": "You can close this window. The application that started this connection will finish setting things up shortly.", + "completedTitle": "Connected!", + "completedDescription": "You can close this window now.", + "failedTitle": "Connection failed.", + "failedDescription": "Something went wrong while connecting. Please try again from the application that started this.", + "cancelledTitle": "Connection cancelled.", + "cancelledDescription": "You can close this window.", + "expiredTitle": "This link has expired.", + "expiredDescription": "Please start the connection again from the application that started this.", + "invalidTitle": "This link is no longer valid.", + "invalidDescription": "This connection link is invalid or has already been used." } } diff --git a/apps/builder/messages/tr.json b/apps/builder/messages/tr.json index 25743dae84..960614a5b1 100644 --- a/apps/builder/messages/tr.json +++ b/apps/builder/messages/tr.json @@ -2304,8 +2304,7 @@ "broadcasts": "Yayınlar", "analytics": "Analitik", "ecommerce": "E-ticaret", - "integrations": "Entegrasyonlar", - "channels": "Kanallar", + "connections": "Bağlantılar", "minigames": "Mini Oyunlar", "appointments": "Randevular", "media": "Medya", @@ -6089,5 +6088,19 @@ }, "accountFields": { "title": "Bot Alanları" + }, + "connectSessionPage": { + "processingTitle": "Finishing up…", + "processingDescription": "You can close this window. The application that started this connection will finish setting things up shortly.", + "completedTitle": "Connected!", + "completedDescription": "You can close this window now.", + "failedTitle": "Connection failed.", + "failedDescription": "Something went wrong while connecting. Please try again from the application that started this.", + "cancelledTitle": "Connection cancelled.", + "cancelledDescription": "You can close this window.", + "expiredTitle": "This link has expired.", + "expiredDescription": "Please start the connection again from the application that started this.", + "invalidTitle": "This link is no longer valid.", + "invalidDescription": "This connection link is invalid or has already been used." } } diff --git a/apps/builder/messages/vi.json b/apps/builder/messages/vi.json index a055eb2ce7..94433683b1 100644 --- a/apps/builder/messages/vi.json +++ b/apps/builder/messages/vi.json @@ -2304,8 +2304,7 @@ "broadcasts": "Phát sóng", "analytics": "Phân tích", "ecommerce": "Thương mại điện tử", - "integrations": "Tích hợp", - "channels": "Kênh", + "connections": "Kết nối", "minigames": "Minigame", "appointments": "Lịch hẹn", "media": "Phương tiện", @@ -6089,5 +6088,19 @@ }, "accountFields": { "title": "Trường bot" + }, + "connectSessionPage": { + "processingTitle": "Finishing up…", + "processingDescription": "You can close this window. The application that started this connection will finish setting things up shortly.", + "completedTitle": "Connected!", + "completedDescription": "You can close this window now.", + "failedTitle": "Connection failed.", + "failedDescription": "Something went wrong while connecting. Please try again from the application that started this.", + "cancelledTitle": "Connection cancelled.", + "cancelledDescription": "You can close this window.", + "expiredTitle": "This link has expired.", + "expiredDescription": "Please start the connection again from the application that started this.", + "invalidTitle": "This link is no longer valid.", + "invalidDescription": "This connection link is invalid or has already been used." } } diff --git a/apps/builder/messages/zh-CN.json b/apps/builder/messages/zh-CN.json index 550ab52926..06d70c6662 100644 --- a/apps/builder/messages/zh-CN.json +++ b/apps/builder/messages/zh-CN.json @@ -3005,8 +3005,7 @@ "broadcasts": "群发", "analytics": "分析", "ecommerce": "电子商务", - "integrations": "集成", - "channels": "渠道", + "connections": "连接", "minigames": "小游戏", "appointments": "预约", "media": "媒体", @@ -6089,5 +6088,19 @@ }, "accountFields": { "title": "机器人字段" + }, + "connectSessionPage": { + "processingTitle": "Finishing up…", + "processingDescription": "You can close this window. The application that started this connection will finish setting things up shortly.", + "completedTitle": "Connected!", + "completedDescription": "You can close this window now.", + "failedTitle": "Connection failed.", + "failedDescription": "Something went wrong while connecting. Please try again from the application that started this.", + "cancelledTitle": "Connection cancelled.", + "cancelledDescription": "You can close this window.", + "expiredTitle": "This link has expired.", + "expiredDescription": "Please start the connection again from the application that started this.", + "invalidTitle": "This link is no longer valid.", + "invalidDescription": "This connection link is invalid or has already been used." } } diff --git a/apps/builder/messages/zh-TW.json b/apps/builder/messages/zh-TW.json index 62f7f09300..a3c2694f10 100644 --- a/apps/builder/messages/zh-TW.json +++ b/apps/builder/messages/zh-TW.json @@ -1440,8 +1440,7 @@ "broadcasts": "群發", "analytics": "分析", "ecommerce": "電子商務", - "integrations": "整合", - "channels": "頻道", + "connections": "連線", "minigames": "小遊戲", "appointments": "預約", "media": "媒體", @@ -6089,5 +6088,19 @@ }, "accountFields": { "title": "機器人欄位" + }, + "connectSessionPage": { + "processingTitle": "Finishing up…", + "processingDescription": "You can close this window. The application that started this connection will finish setting things up shortly.", + "completedTitle": "Connected!", + "completedDescription": "You can close this window now.", + "failedTitle": "Connection failed.", + "failedDescription": "Something went wrong while connecting. Please try again from the application that started this.", + "cancelledTitle": "Connection cancelled.", + "cancelledDescription": "You can close this window.", + "expiredTitle": "This link has expired.", + "expiredDescription": "Please start the connection again from the application that started this.", + "invalidTitle": "This link is no longer valid.", + "invalidDescription": "This connection link is invalid or has already been used." } } diff --git a/apps/builder/package.json b/apps/builder/package.json index 8e85cbb637..18474042bb 100644 --- a/apps/builder/package.json +++ b/apps/builder/package.json @@ -26,6 +26,7 @@ "@chatbotx.io/auth": "workspace:*", "@chatbotx.io/automated-response": "workspace:*", "@chatbotx.io/business": "workspace:*", + "@chatbotx.io/connections": "workspace:*", "@chatbotx.io/database": "workspace:*", "@chatbotx.io/encryption": "workspace:*", "@chatbotx.io/event-bus": "workspace:*", diff --git a/apps/builder/src/app/(no-sidebar)/channels/create/messenger/route.ts b/apps/builder/src/app/(no-sidebar)/channels/create/messenger/route.ts index e63a47a2f4..a8e39b681a 100644 --- a/apps/builder/src/app/(no-sidebar)/channels/create/messenger/route.ts +++ b/apps/builder/src/app/(no-sidebar)/channels/create/messenger/route.ts @@ -3,32 +3,31 @@ import { workspaceService, } from "@chatbotx.io/business" import "@chatbotx.io/business/audit" -import { cookies } from "next/headers" +import { connectSessionService } from "@chatbotx.io/business/connect-session" +import { connectionService } from "@chatbotx.io/connections" +import type { AuthValue } from "@chatbotx.io/sdk" import { notFound, redirect } from "next/navigation" import type { NextRequest } from "next/server" -import { - buildMessengerReferer, - generateMessengerRedirectUri, -} from "@/features/integration-messenger/libs/oauth" +import { resolveOAuthCredential } from "@/features/connections/lib/resolve-connect-credential" import { tryReuseFacebookSsoToken } from "@/features/integration-messenger/libs/sso-reuse" import { requireWorkspacePermission } from "@/lib/auth/require-workspace-permission" import { getCurrentUserId } from "@/lib/auth/utils" -import { - encryptAuth, - FB_MESSENGER_PENDING_AUTH_COOKIE, - FB_PENDING_AUTH_MAX_AGE, - writePendingAuth, -} from "@/lib/facebook-pending-auth" import { resolvePlatformOwnerId } from "@/lib/platform-credential-owner" import { createFirstWorkspace } from "@/lib/workspace/create-first-workspace" /** * Reached only via a redirect from `/channels/create?channel=messenger` - * (never linked to directly). The Facebook SSO token reuse check needs to set - * the pending-auth cookie on a hit, and cookies can only be written from a - * Server Action or a Route Handler — never from a Server Component's render, - * which is what `/channels/create` is. This route re-runs the auth/workspace - * guards itself since it's a public GET endpoint, not just an internal helper. + * (never linked to directly). The Facebook SSO token reuse check needs to + * mint a `ConnectSession` synchronously on a hit — no OAuth round-trip, so + * there is no callback to build it in — and that write can't happen from a + * Server Component's render. This route re-runs the auth/workspace guards + * itself since it's a public GET endpoint, not just an internal helper. + * + * Both branches resolve (or create) the target workspace up front, unlike + * the legacy cookie-based flow which deferred that to the OAuth callback: + * `ConnectSession.workspaceId` is a required column, so a session cannot be + * minted for a not-yet-existing workspace the way the old base64 `state` + * blob could carry an absent `workspaceId` and let the callback create one. */ export async function GET(req: NextRequest) { const workspaceId = req.nextUrl.searchParams.get("workspaceId") ?? undefined @@ -52,35 +51,64 @@ export async function GET(req: NextRequest) { return notFound() } + const targetWorkspace = workspaceId + ? await workspaceService.findById({ id: workspaceId }) + : await createFirstWorkspace(userId) + const reuse = await tryReuseFacebookSsoToken({ userId, messengerCredential: messenger.config, }) if (reuse.reusable) { - // Mirror the OAuth callback's workspace resolution (`callback.ts`): a - // channel connect can be the very first one for a user, with no - // workspace yet — create it now instead of writing a pending-auth cookie - // that points nowhere. - const targetWorkspace = workspaceId - ? await workspaceService.findById({ id: workspaceId }) - : await createFirstWorkspace(userId) - const referer = await buildMessengerReferer(targetWorkspace.id) - const token = await encryptAuth({ - userToken: reuse.userToken, - userId: reuse.userId, - userName: reuse.userName, - userAvatarUrl: reuse.userAvatarUrl, + const { session } = await connectSessionService.create({ workspaceId: targetWorkspace.id, - referer, - version: messenger.config.version, - expiresAt: Date.now() + FB_PENDING_AUTH_MAX_AGE * 1000, + provider: "messenger", + purpose: "connect", + actorUserId: userId, + platformOwnerId, }) - const cookieStore = await cookies() - writePendingAuth(cookieStore, FB_MESSENGER_PENDING_AUTH_COOKIE, token) - redirect("/channels/messenger/select") + const auth: AuthValue = { + authType: "oauth2", + clientId: messenger.config.clientId, + clientSecret: messenger.config.clientSecret, + redirectUrl: "", + version: messenger.config.version, + tokens: { accessToken: reuse.userToken }, + } + await connectionService.listAndAttachCandidates(session, auth) + redirect(`/channels/messenger/select?session=${session.id}`) } - const redirectUri = await generateMessengerRedirectUri(messenger, workspaceId) - redirect(redirectUri) + const resolved = await resolveOAuthCredential({ + provider: "messenger", + ownerId: platformOwnerId, + }) + if (!resolved) { + return notFound() + } + + const { session, nextAction } = await connectionService.startSession({ + workspaceId: targetWorkspace.id, + provider: "messenger", + purpose: "connect", + credential: resolved.credential, + callbackUrl: resolved.callbackUrl, + actorUserId: userId, + platformOwnerId, + }) + // The session's own id isn't known until `startSession` returns, so the + // page-picker redirect target — which the select page needs to resolve + // this same session via `?session=` — is set in a follow-up call rather + // than passed into `startSession` itself. + await connectSessionService.updateReturnUrl({ + id: session.id, + returnUrl: `/channels/messenger/select?session=${session.id}`, + }) + if (nextAction.type !== "open_url") { + throw new Error( + `Unexpected connect next action for messenger: ${nextAction.type}`, + ) + } + redirect(nextAction.url) } diff --git a/apps/builder/src/app/(no-sidebar)/channels/create/page.tsx b/apps/builder/src/app/(no-sidebar)/channels/create/page.tsx index f1bf5149a0..4cca714c5c 100644 --- a/apps/builder/src/app/(no-sidebar)/channels/create/page.tsx +++ b/apps/builder/src/app/(no-sidebar)/channels/create/page.tsx @@ -5,8 +5,6 @@ import { notFound, redirect } from "next/navigation" import InboxSelectCard from "@/features/inboxes/components/inbox-select-card" import { CreateApiForm } from "@/features/integration-api/components/create-api-form" import { InstagramLoginSelect } from "@/features/integration-instagram/components/instagram-login-select" -import { generateInstagramRedirectUri } from "@/features/integration-instagram/libs/oauth" -import { generateInstagramFacebookRedirectUri } from "@/features/integration-instagram/libs/oauth-facebook" import { TelegramConnect } from "@/features/integration-telegram/components/telegram-connect" import { generateTiktokRedirectUri } from "@/features/integration-tiktok/libs/tiktok" import { SimpleCreateWebchat } from "@/features/integration-webchat/simple-create-webchat" @@ -137,11 +135,9 @@ export default async function CreateChannelPage(props: CreateChannelPageProps) { instagram && isVisible("instagram") ) { - const redirectUri = await generateInstagramRedirectUri( - instagram, - workspaceId, + redirect( + `/channels/instagram${workspaceId ? `?workspaceId=${workspaceId}` : ""}`, ) - redirect(redirectUri) } // `instagram-facebook` is a login-flavor route discriminator, not its own @@ -152,11 +148,9 @@ export default async function CreateChannelPage(props: CreateChannelPageProps) { instagramFacebook && isVisible("instagram") ) { - const redirectUri = await generateInstagramFacebookRedirectUri( - instagramFacebook, - workspaceId, + redirect( + `/channels/instagram-facebook${workspaceId ? `?workspaceId=${workspaceId}` : ""}`, ) - redirect(redirectUri) } if (selectedChannel === "zalo" && zalo && isVisible("zalo")) { diff --git a/apps/builder/src/app/(no-sidebar)/channels/instagram-facebook/route.ts b/apps/builder/src/app/(no-sidebar)/channels/instagram-facebook/route.ts new file mode 100644 index 0000000000..5151e355b9 --- /dev/null +++ b/apps/builder/src/app/(no-sidebar)/channels/instagram-facebook/route.ts @@ -0,0 +1,68 @@ +import { workspaceService } from "@chatbotx.io/business" +import "@chatbotx.io/business/audit" +import { connectSessionService } from "@chatbotx.io/business/connect-session" +import { connectionService } from "@chatbotx.io/connections" +import { notFound, redirect } from "next/navigation" +import type { NextRequest } from "next/server" +import { resolveOAuthCredential } from "@/features/connections/lib/resolve-connect-credential" +import { requireWorkspacePermission } from "@/lib/auth/require-workspace-permission" +import { getCurrentUserId } from "@/lib/auth/utils" +import { resolvePlatformOwnerId } from "@/lib/platform-credential-owner" +import { createFirstWorkspace } from "@/lib/workspace/create-first-workspace" + +/** + * Reached only via a redirect from `/channels/create?channel=instagram-facebook` + * (never linked to directly). Extracted out of `channels/create/page.tsx`'s + * inline `generateInstagramFacebookRedirectUri` call — see the sibling + * `channels/instagram/route.ts` for why this can't stay inline. + */ +export async function GET(req: NextRequest) { + const workspaceId = req.nextUrl.searchParams.get("workspaceId") ?? undefined + + if (workspaceId) { + await requireWorkspacePermission(workspaceId, "superAdmin") + } + + const userId = await getCurrentUserId() + if (!userId) { + return notFound() + } + + const platformOwnerId = await resolvePlatformOwnerId({ userId, workspaceId }) + + const resolved = await resolveOAuthCredential({ + provider: "instagramFacebook", + ownerId: platformOwnerId, + }) + if (!resolved) { + return notFound() + } + + const targetWorkspace = workspaceId + ? await workspaceService.findById({ id: workspaceId }) + : await createFirstWorkspace(userId) + + const { session, nextAction } = await connectionService.startSession({ + workspaceId: targetWorkspace.id, + provider: "instagramFacebook", + purpose: "connect", + credential: resolved.credential, + callbackUrl: resolved.callbackUrl, + actorUserId: userId, + platformOwnerId, + }) + // The session's own id isn't known until `startSession` returns, so the + // page-picker redirect target — which the select page needs to resolve + // this same session via `?session=` — is set in a follow-up call rather + // than passed into `startSession` itself. + await connectSessionService.updateReturnUrl({ + id: session.id, + returnUrl: `/channels/instagram-facebook/select?session=${session.id}`, + }) + if (nextAction.type !== "open_url") { + throw new Error( + `Unexpected connect next action for instagramFacebook: ${nextAction.type}`, + ) + } + redirect(nextAction.url) +} diff --git a/apps/builder/src/app/(no-sidebar)/channels/instagram-facebook/select/page.tsx b/apps/builder/src/app/(no-sidebar)/channels/instagram-facebook/select/page.tsx index 0f17e5e668..d4fea2d26f 100644 --- a/apps/builder/src/app/(no-sidebar)/channels/instagram-facebook/select/page.tsx +++ b/apps/builder/src/app/(no-sidebar)/channels/instagram-facebook/select/page.tsx @@ -1,5 +1,3 @@ -import { instagramIntegrationService } from "@chatbotx.io/business" -import { getUserInstagramAccounts } from "@chatbotx.io/integration-instagram-facebook" import { Card, CardContent, @@ -10,64 +8,45 @@ import Image from "next/image" import { redirect } from "next/navigation" import { getTranslations } from "next-intl/server" import { CONNECT_PICKER_CARD_CLASS } from "@/features/channel-connect/components/connect-picker-card" -import { - type ConnectPickerItem, - markAlreadyConnected, - rankPickerItem, -} from "@/features/channel-connect/lib/picker-items" +import type { ConnectPickerItem } from "@/features/channel-connect/lib/picker-items" +import { resolveConnectSession } from "@/features/channel-connect/lib/resolve-connect-session" import { InboxIcon } from "@/features/inboxes/components/inbox-icon" import { SelectFacebookAccounts } from "@/features/integration-instagram/components/select-facebook-accounts" -import { - FB_INSTAGRAM_FACEBOOK_PENDING_AUTH_COOKIE, - readPendingAuth, -} from "@/lib/facebook-pending-auth" +import { getCurrentUserId } from "@/lib/auth/utils" export const dynamic = "force-dynamic" /** - * `getUserInstagramAccounts` already narrows the provider list to accounts - * linked to a Page the user administers (`/me/accounts`), so — unlike - * Messenger's pages — there is no "not an admin" rank; only selectable (0) - * vs. already-connected (2) ever occurs. - */ -const DISABLED_REASON_KEY_BY_RANK: Record = { - 0: undefined, - 2: "instagram.selectPage.alreadyConnectedNote", -} - -/** - * A row is only selectable when it isn't already connected elsewhere. Its - * page access token is never sent to the client at all: the connect action - * re-fetches the provider list itself from the pending-auth cookie (plan - * §4.7/§4.9). `rank` is computed once per account by the caller (used for - * both sorting and the disabled-reason lookup) instead of being re-derived - * here a second time. + * `session.targets` is already narrowed to accounts linked to a Page the + * user administers (Instagram-Facebook's `listCandidates` mirrors + * `getUserInstagramAccounts`'s `/me/accounts` scoping) — no live re-fetch, + * and (like Messenger) no "not admin" rank: only selectable vs. + * already-connected. */ function toPickerItem( - account: { + target: { id: string name: string - username: string - profile_picture_url?: string - isAlreadyConnected: boolean + avatarUrl?: string + selectable: boolean + alreadyConnected?: "this_workspace" | "other_workspace" }, - rank: number, t: Awaited>, ): ConnectPickerItem { - const disabledReasonKey = DISABLED_REASON_KEY_BY_RANK[rank] - return { - id: account.id, - name: account.name, - secondary: `@${account.username}`, - disabled: rank !== 0, - disabledReason: disabledReasonKey ? t(disabledReasonKey) : undefined, - leading: account.profile_picture_url ? ( + id: target.id, + name: target.name, + secondary: target.id, + disabled: !target.selectable, + disabledReason: target.alreadyConnected + ? t("instagram.selectPage.alreadyConnectedNote") + : undefined, + leading: target.avatarUrl ? ( {account.name} ) : ( @@ -76,30 +55,32 @@ function toPickerItem( } } -export default async function InstagramFacebookSelectPage() { - const auth = await readPendingAuth(FB_INSTAGRAM_FACEBOOK_PENDING_AUTH_COOKIE) - - if (!auth) { +export default async function InstagramFacebookSelectPage({ + searchParams, +}: { + searchParams: Promise<{ session?: string }> +}) { + const { session: sessionId } = await searchParams + if (!sessionId) { redirect("/channels/create") } - const accounts = await getUserInstagramAccounts(auth.userToken, auth.version) + const userId = await getCurrentUserId() + if (!userId) { + redirect("/channels/create") + } - const connectedIgIds = await instagramIntegrationService.findConnectedIgIds( - accounts.map((account) => account.id), - ) + const resolved = await resolveConnectSession({ + userId, + sessionId, + credentialType: "instagramFacebook", + brandingChannel: "instagram", + }) const t = await getTranslations() - const items = markAlreadyConnected(accounts, connectedIgIds) - .map((account) => ({ - account, - rank: rankPickerItem({ - isConnectable: true, - isAlreadyConnected: account.isAlreadyConnected, - }), - })) - .sort((current, next) => current.rank - next.rank) - .map(({ account, rank }) => toPickerItem(account, rank, t)) + const items = resolved.session.targets + .map((target) => toPickerItem(target, t)) + .sort((current, next) => Number(current.disabled) - Number(next.disabled)) return ( @@ -107,7 +88,11 @@ export default async function InstagramFacebookSelectPage() { {t("fields.instagram.connectViaFacebook")} - + ) diff --git a/apps/builder/src/app/(no-sidebar)/channels/instagram/route.ts b/apps/builder/src/app/(no-sidebar)/channels/instagram/route.ts new file mode 100644 index 0000000000..812df73388 --- /dev/null +++ b/apps/builder/src/app/(no-sidebar)/channels/instagram/route.ts @@ -0,0 +1,70 @@ +import { workspaceService } from "@chatbotx.io/business" +import "@chatbotx.io/business/audit" +import { connectSessionService } from "@chatbotx.io/business/connect-session" +import { connectionService } from "@chatbotx.io/connections" +import { notFound, redirect } from "next/navigation" +import type { NextRequest } from "next/server" +import { resolveOAuthCredential } from "@/features/connections/lib/resolve-connect-credential" +import { requireWorkspacePermission } from "@/lib/auth/require-workspace-permission" +import { getCurrentUserId } from "@/lib/auth/utils" +import { resolvePlatformOwnerId } from "@/lib/platform-credential-owner" +import { createFirstWorkspace } from "@/lib/workspace/create-first-workspace" + +/** + * Reached only via a redirect from `/channels/create?channel=instagram-direct` + * (never linked to directly). Extracted out of `channels/create/page.tsx`'s + * inline `generateInstagramRedirectUri` call — a Server Component's render + * can't itself start a `ConnectSession` down a path that could redirect to + * `/channels/create?error=...` (see `createFirstWorkspace`), so this mirrors + * Messenger's dedicated create route. + */ +export async function GET(req: NextRequest) { + const workspaceId = req.nextUrl.searchParams.get("workspaceId") ?? undefined + + if (workspaceId) { + await requireWorkspacePermission(workspaceId, "superAdmin") + } + + const userId = await getCurrentUserId() + if (!userId) { + return notFound() + } + + const platformOwnerId = await resolvePlatformOwnerId({ userId, workspaceId }) + + const resolved = await resolveOAuthCredential({ + provider: "instagram", + ownerId: platformOwnerId, + }) + if (!resolved) { + return notFound() + } + + const targetWorkspace = workspaceId + ? await workspaceService.findById({ id: workspaceId }) + : await createFirstWorkspace(userId) + + const { session, nextAction } = await connectionService.startSession({ + workspaceId: targetWorkspace.id, + provider: "instagram", + purpose: "connect", + credential: resolved.credential, + callbackUrl: resolved.callbackUrl, + actorUserId: userId, + platformOwnerId, + }) + // The session's own id isn't known until `startSession` returns, so the + // confirm-screen redirect target — which the select page needs to resolve + // this same session via `?session=` — is set in a follow-up call rather + // than passed into `startSession` itself. + await connectSessionService.updateReturnUrl({ + id: session.id, + returnUrl: `/channels/instagram/select?session=${session.id}`, + }) + if (nextAction.type !== "open_url") { + throw new Error( + `Unexpected connect next action for instagram: ${nextAction.type}`, + ) + } + redirect(nextAction.url) +} diff --git a/apps/builder/src/app/(no-sidebar)/channels/instagram/select/page.tsx b/apps/builder/src/app/(no-sidebar)/channels/instagram/select/page.tsx index 78d3fafe65..5a2581ac7a 100644 --- a/apps/builder/src/app/(no-sidebar)/channels/instagram/select/page.tsx +++ b/apps/builder/src/app/(no-sidebar)/channels/instagram/select/page.tsx @@ -1,25 +1,51 @@ -import { getInstagramAccount } from "@chatbotx.io/integration-instagram" import { redirect } from "next/navigation" +import { resolveConnectSession } from "@/features/channel-connect/lib/resolve-connect-session" import { SelectAccount } from "@/features/integration-instagram/components/select-accounts" -import { - FB_INSTAGRAM_PENDING_AUTH_COOKIE, - readPendingAuth, -} from "@/lib/facebook-pending-auth" +import { getCurrentUserId } from "@/lib/auth/utils" export const dynamic = "force-dynamic" -export default async function InstagramSelectPage() { - const auth = await readPendingAuth(FB_INSTAGRAM_PENDING_AUTH_COOKIE) +/** + * `session.targets` always has exactly one entry for the direct-login + * provider (its `exchangeCode` returns the final per-account auth directly, + * with no `listCandidates` step) — no live `getInstagramAccount` re-fetch. + */ +export default async function InstagramSelectPage({ + searchParams, +}: { + searchParams: Promise<{ session?: string }> +}) { + const { session: sessionId } = await searchParams + if (!sessionId) { + redirect("/channels/create") + } - if (!auth) { + const userId = await getCurrentUserId() + if (!userId) { redirect("/channels/create") } - const account = await getInstagramAccount(auth.userToken) + const resolved = await resolveConnectSession({ + userId, + sessionId, + credentialType: "instagram", + brandingChannel: "instagram", + }) - if (!account) { + const target = resolved.session.targets[0] + if (!target) { redirect("/channels/create") } - return + return ( + + ) } diff --git a/apps/builder/src/app/(no-sidebar)/channels/messenger/select/page.tsx b/apps/builder/src/app/(no-sidebar)/channels/messenger/select/page.tsx index c73d61ab28..a8cf05cbcc 100644 --- a/apps/builder/src/app/(no-sidebar)/channels/messenger/select/page.tsx +++ b/apps/builder/src/app/(no-sidebar)/channels/messenger/select/page.tsx @@ -1,86 +1,83 @@ -import { messengerIntegrationService } from "@chatbotx.io/business" -import { getUserPages } from "@chatbotx.io/integration-messenger" -import type { ConnectableFacebookPage } from "@chatbotx.io/integration-messenger/schema" import { redirect } from "next/navigation" import { getTranslations } from "next-intl/server" -import { - markAlreadyConnected, - rankPickerItem, -} from "@/features/channel-connect/lib/picker-items" +import { resolveConnectSession } from "@/features/channel-connect/lib/resolve-connect-session" import { InboxIcon } from "@/features/inboxes/components/inbox-icon" import type { MessengerPickerItem } from "@/features/integration-messenger/components/messenger-pages" import { SelectPage } from "@/features/integration-messenger/components/select-account" -import { - FB_MESSENGER_PENDING_AUTH_COOKIE, - readPendingAuth, -} from "@/lib/facebook-pending-auth" +import { getCurrentUserId } from "@/lib/auth/utils" export const dynamic = "force-dynamic" /** - * `rankPickerItem`'s rank (0 selectable / 1 not-admin / 2 already-connected) - * doubles as the disabled-reason lookup, table-driven instead of an - * if/else-if chain re-deriving the same precedence rank already computes. - */ -const DISABLED_REASON_KEY_BY_RANK: Record = { - 0: undefined, - 1: "messenger.selectPage.notAdminNote", - 2: "messenger.selectPage.alreadyConnectedNote", -} - -/** - * A page is only selectable when the user has full admin permission on it - * and it isn't already connected elsewhere. Every other page is rendered - * disabled — its `access_token` is never sent to the client at all: the - * connect action re-fetches the provider list itself from the pending-auth - * cookie (plan §4.7/§4.9). + * `session.targets` is already the fully-computed, admin-filtered, + * already-connected-marked list `ConnectionService.listAndAttachCandidates` + * built at authorization time — there is no live `getUserPages` re-fetch + * here anymore. Two documented, minor scope reductions versus the + * pending-auth-cookie version of this page: + * - No "not admin" rank: Messenger's `listCandidates` silently drops pages + * the user doesn't administer before they ever become a target, so + * `messenger-pages.tsx`'s "you're not an admin on any page" warning banner + * can no longer fire — a user who administers zero pages simply sees an + * empty picker instead. + * - `bmLookupFailed` (the Business Manager lookup warning) isn't part of + * the session's public target projection, so it's always `false` here. */ function toPickerItem( - page: ConnectableFacebookPage & { isAlreadyConnected: boolean }, + target: { + id: string + name: string + selectable: boolean + alreadyConnected?: "this_workspace" | "other_workspace" + }, t: Awaited>, ): MessengerPickerItem { - const rank = rankPickerItem(page) - const disabledReasonKey = DISABLED_REASON_KEY_BY_RANK[rank] - return { - id: page.id, - name: page.name, - secondary: page.id, - disabled: rank !== 0, - disabledReason: disabledReasonKey ? t(disabledReasonKey) : undefined, + id: target.id, + name: target.name, + secondary: target.id, + disabled: !target.selectable, + disabledReason: target.alreadyConnected + ? t("messenger.selectPage.alreadyConnectedNote") + : undefined, leading: , - isConnectable: page.isConnectable, - isAlreadyConnected: page.isAlreadyConnected, + isConnectable: true, + isAlreadyConnected: Boolean(target.alreadyConnected), } } -export default async function MessengerSelectPage() { - const pendingAuth = await readPendingAuth(FB_MESSENGER_PENDING_AUTH_COOKIE) - - if (!pendingAuth) { +export default async function MessengerSelectPage({ + searchParams, +}: { + searchParams: Promise<{ session?: string }> +}) { + const { session: sessionId } = await searchParams + if (!sessionId) { redirect("/channels/create") } - const { pages, bmLookupFailed } = await getUserPages( - pendingAuth.userToken, - pendingAuth.version, - ) + const userId = await getCurrentUserId() + if (!userId) { + redirect("/channels/create") + } - const connectedPageIds = - await messengerIntegrationService.findConnectedPageIds( - pages.map((page) => page.id), - ) + const resolved = await resolveConnectSession({ + userId, + sessionId, + credentialType: "messenger", + brandingChannel: "messenger", + }) const t = await getTranslations() - const items = markAlreadyConnected(pages, connectedPageIds) - .sort((current, next) => rankPickerItem(current) - rankPickerItem(next)) - .map((page) => toPickerItem(page, t)) + const items = resolved.session.targets + .map((target) => toPickerItem(target, t)) + .sort((current, next) => Number(current.disabled) - Number(next.disabled)) return ( ) } diff --git a/apps/builder/src/app/connect/[sessionId]/auto-refresh.tsx b/apps/builder/src/app/connect/[sessionId]/auto-refresh.tsx new file mode 100644 index 0000000000..c6c883e744 --- /dev/null +++ b/apps/builder/src/app/connect/[sessionId]/auto-refresh.tsx @@ -0,0 +1,29 @@ +"use client" + +import { useRouter } from "next/navigation" +import { useEffect } from "react" + +const POLL_INTERVAL_MS = 3000 + +/** + * Re-renders the server component on an interval while a `ConnectSession` + * is still `pending`/`awaiting_selection` — the API/MCP caller that started + * the connect finishes it from their own side (exchanging the code, then + * calling `connectTargets`), so this page has nothing to do but wait and + * pick up the eventual `completed`/`failed` status. Stops re-rendering once + * the page itself renders a terminal status (this component is only mounted + * for the in-flight branch, so unmounting it — a fresh server render — ends + * the polling naturally). + */ +export function ConnectSessionAutoRefresh() { + const router = useRouter() + + useEffect(() => { + const timer = setInterval(() => { + router.refresh() + }, POLL_INTERVAL_MS) + return () => clearInterval(timer) + }, [router]) + + return null +} diff --git a/apps/builder/src/app/connect/[sessionId]/page.tsx b/apps/builder/src/app/connect/[sessionId]/page.tsx new file mode 100644 index 0000000000..f58797a6c7 --- /dev/null +++ b/apps/builder/src/app/connect/[sessionId]/page.tsx @@ -0,0 +1,71 @@ +import { connectSessionService } from "@chatbotx.io/business/connect-session" +import type { Metadata } from "next" +import { getTranslations } from "next-intl/server" +import { ConnectSessionAutoRefresh } from "./auto-refresh" +import { resolveConnectSessionMessageKind } from "./resolve-message" + +export const metadata: Metadata = { + title: "Connecting…", +} + +type ConnectSessionPageProps = { + params: Promise<{ sessionId: string }> +} + +/** + * Neutral completion page for an OAuth `ConnectSession` started via the + * public/private connect API (`POST /v1/connections`, + * `POST /v1/connections/{id}/reconnect`) with no `redirectUrl` (or one that + * failed `sanitizeOptionalReturnUrl`). No builder login is required: the + * person completing an API/MCP-started connect is never necessarily a + * builder user or a member of the workspace that started it — `sessionId` + * itself is the capability token (an unguessable snowflake), matched + * against `ConnectSessionService.findById`'s workspace-unscoped read. + * + * This page never lets the visitor pick a target for a multi-account + * provider — that is the API/MCP caller's own job, via their own + * `POST /v1/connect-sessions/{id}/targets` call once they observe + * `awaiting_selection` through their own polling. This page only reports + * status and auto-refreshes while the session is still in flight. + * + * WhatsApp's embedded-signup popup is NOT hosted here — WhatsApp's connect + * flow creates workspaces and manages its own `WhatsappSignupSession` + * outside the Connection domain and is out of scope for this page. + */ +export default async function ConnectSessionPage( + props: ConnectSessionPageProps, +) { + const { sessionId } = await props.params + const t = await getTranslations("connectSessionPage") + const session = await connectSessionService.findById(sessionId) + const kind = resolveConnectSessionMessageKind(session?.status ?? null) + + return ( + + {kind === "processing" && } + + ) +} + +function ConnectSessionMessage({ + title, + description, + children, +}: { + title: string + description: string + children?: React.ReactNode +}) { + return ( +
+
+

{title}

+

{description}

+ {children} +
+
+ ) +} diff --git a/apps/builder/src/app/connect/[sessionId]/resolve-message.ts b/apps/builder/src/app/connect/[sessionId]/resolve-message.ts new file mode 100644 index 0000000000..d799be49e9 --- /dev/null +++ b/apps/builder/src/app/connect/[sessionId]/resolve-message.ts @@ -0,0 +1,46 @@ +import type { ConnectSessionStatus } from "@chatbotx.io/database/partials" + +/** + * Which message block `/connect/{sessionId}` shows for a given + * `ConnectSession.status` (or `null` when the session id resolved to + * nothing at all). `"pending"`/`"authorized"`/`"awaiting_selection"` are + * ALL still-in-flight states — including `"authorized"`, the brief window + * between a successful OAuth exchange and candidate listing — so they all + * map to `"processing"` (auto-refreshing), never `"failed"`. Every other + * value (including a genuinely unknown future status) falls back to + * `"failed"` rather than silently misreporting progress as success. + * + * Extracted as a pure function so this mapping — the one behavior in this + * page with real branching to get wrong — is unit-testable without + * `@testing-library/react` (not a dependency of this app) or a React + * render harness. + */ +export type ConnectSessionMessageKind = + | "invalid" + | "processing" + | "completed" + | "cancelled" + | "expired" + | "failed" + +export function resolveConnectSessionMessageKind( + status: ConnectSessionStatus | null, +): ConnectSessionMessageKind { + if (!status) { + return "invalid" + } + switch (status) { + case "pending": + case "authorized": + case "awaiting_selection": + return "processing" + case "completed": + return "completed" + case "cancelled": + return "cancelled" + case "expired": + return "expired" + default: + return "failed" + } +} diff --git a/apps/builder/src/app/integrations/[...integration]/callback.ts b/apps/builder/src/app/integrations/[...integration]/callback.ts index a699e8f851..7d07108ae7 100644 --- a/apps/builder/src/app/integrations/[...integration]/callback.ts +++ b/apps/builder/src/app/integrations/[...integration]/callback.ts @@ -11,6 +11,11 @@ import { workspaceService, } from "@chatbotx.io/business" import { auditService, withAuditContext } from "@chatbotx.io/business/audit" +import { connectSessionService } from "@chatbotx.io/business/connect-session" +import { + CONNECTION_REGISTRY, + connectionService, +} from "@chatbotx.io/connections" import { db } from "@chatbotx.io/database/client" import { type IntegrationType, @@ -27,16 +32,7 @@ import { type FacebookAdsAuthValue, } from "@chatbotx.io/integration-facebook-ads" import { exchangeCodeForToken as exchangeInstagramCode } from "@chatbotx.io/integration-instagram" -import { - exchangeCodeForToken as exchangeInstagramFacebookCode, - getFacebookUser as getInstagramFacebookUser, -} from "@chatbotx.io/integration-instagram-facebook" -import { - exchangeCodeForToken as exchangeMessengerCode, - type FacebookUser, - getFacebookUser as getMessengerFacebookUser, -} from "@chatbotx.io/integration-messenger" -import { exchangeLongLivedToken as exchangeMessengerLongLivedToken } from "@chatbotx.io/integration-messenger/apis/page" +import { exchangeCodeForToken as exchangeInstagramFacebookCode } from "@chatbotx.io/integration-instagram-facebook" import type { MetaCatalogAuthValue } from "@chatbotx.io/integration-meta-catalog/schemas" import { AuthType, @@ -48,7 +44,6 @@ import { getPublicUrlFromRequest, zodBigintAsString, } from "@chatbotx.io/utils" -import { cookies } from "next/headers" import { notFound, redirect } from "next/navigation" import type { NextRequest } from "next/server" import { normalizeError } from "universal-error-normalizer" @@ -67,14 +62,6 @@ import { integrations } from "@/integration" import { assertWorkspaceSuperAdmin } from "@/lib/auth/assert-workspace-super-admin" import { getCurrentUser } from "@/lib/auth/utils" import { buildReconnectRedirectUrl } from "@/lib/channel-reconnect" -import { - encryptAuth, - FB_INSTAGRAM_FACEBOOK_PENDING_AUTH_COOKIE, - FB_INSTAGRAM_PENDING_AUTH_COOKIE, - FB_MESSENGER_PENDING_AUTH_COOKIE, - FB_PENDING_AUTH_MAX_AGE, - writePendingAuth, -} from "@/lib/facebook-pending-auth" import { logger } from "@/lib/log" import { resolveRelayTarget, sanitizeReferer } from "@/lib/oauth-referer" import { resolveOwnerForWorkspace } from "@/lib/platform-credential-owner" @@ -233,17 +220,117 @@ const storeMetaCatalogConnection = async (args: { }) } -// Best-effort: the connect flow works without the user identity, so a failed -// lookup only leaves `userInfo` unset on the integration row. -const lookupFacebookUser = async ( - fetchUser: () => Promise, -): Promise => { +const CONNECT_SESSION_STATE_PATTERN = /^\d+\.[A-Za-z0-9_-]+$/ + +/** + * Dispatches an OAuth callback whose `state` is a raw `"{sessionId}.{nonce}"` + * string — the Connection-domain `ConnectSession` flow (`POST + * /v1/connections`, `POST /v1/connections/{id}/reconnect`, and the builder + * pickers once converted). Unlike the legacy JSON-state flow below, this + * path needs no builder session cookie and no host-relay hop: every fact it + * needs (`workspaceId`, `provider`, `platformOwnerId`, `returnUrl`) lives on + * the `ConnectSession` row itself — resolved once at `startSession` time — + * not derived from the request's host or an authenticated user, so a + * completion landing on the broker or a reseller's custom domain both + * resolve identically with no relay needed. The completion page + * (`/connect/{id}`) does not require a signed-in builder session either. + */ +const handleConnectSessionCallback = async (url: URL, rawState: string) => { + const [sessionId, nonce] = rawState.split(".") + if (!(sessionId && nonce)) { + return notFound() + } + + const session = await connectSessionService.findByNonce(nonce) + if (!session || session.id !== sessionId) { + logger.debug({ sessionId }, "connect session state could not be verified") + return notFound() + } + + const fallbackReturnUrl = `/connect/${session.id}` + const returnUrl = session.returnUrl + ? await sanitizeReferer(session.returnUrl) + : fallbackReturnUrl + + // Facebook/Google/Zalo/TikTok all return ?error=... when the user cancels + // the OAuth dialog — no code exchange to attempt. + if (url.searchParams.get("error")) { + await connectSessionService.fail({ + id: session.id, + errorCode: "provider_denied", + }) + return redirect(returnUrl) + } + + const adapter = CONNECTION_REGISTRY[session.provider] + if (!(adapter?.credentialType && session.platformOwnerId)) { + logger.warn( + { sessionId: session.id, provider: session.provider }, + "connect session provider is not OAuth-configured", + ) + return notFound() + } + + const credential = await platformCredentialService.resolveForOwner({ + ownerId: session.platformOwnerId, + type: adapter.credentialType, + }) + if (!credential) { + return notFound() + } + + const code = url.searchParams.get("code") ?? "" + // Reconstructs the exact redirect_uri the provider was given at + // `authorizeUrl` time — `buildProviderCallbackUrl` resolved it once + // against this same credential, and the callback always lands on that + // registered host/path (no relay hop for this flow, see above). + const callbackUrl = `${url.origin}${url.pathname}` + try { - return await fetchUser() - } catch (error) { - logger.info({ err: error }, "Failed to fetch Facebook user profile") - return + const completed = await connectionService.completeAuthorization({ + sessionId: session.id, + nonce, + code, + callbackUrl, + credential: credential.config, + }) + + // A non-multi-account provider's grant always resolves to exactly one + // selectable target — finish the connect immediately so an API/MCP + // caller (one with no builder session, `actorTokenId` set) never has to + // make a second `targets` call for it. A builder-initiated session + // (`actorUserId` set) skips this: its picker page — Instagram's direct + // login is non-multi-account too, but still shows a confirm screen with + // per-row coexist/sync-history opt-ins the person must set before the + // connect actually runs — owns finishing the connect itself via its own + // `connectTargets` call from the "Continue" button. + if ( + completed.status === "awaiting_selection" && + !adapter.provider.multiAccount && + !completed.actorUserId + ) { + const onlyTarget = completed.targets[0] + if (onlyTarget?.selectable) { + await connectionService.connectTargets({ + sessionId: completed.id, + workspaceId: completed.workspaceId, + targetIds: [onlyTarget.id], + }) + } + } + } catch (err) { + // `completeAuthorization` already marks the session `failed` for its own + // known error paths (exchange rejected, no candidates, state mismatch); + // this catch is only a safety net so an unexpected error still redirects + // the browser instead of rendering a 500 — the completion page shows + // whatever status the session actually ended up in. + logger.warn( + { err, sessionId: session.id, provider: session.provider }, + "connect session completeAuthorization failed", + ) } + + return redirect(returnUrl) } export const handleCallback = async ( @@ -256,11 +343,22 @@ export const handleCallback = async ( // Parse state params to get workspace info const url = new URL(getPublicUrlFromRequest(req)) + const rawStateParam = url.searchParams.get("state") ?? "" + + // New Connection-domain sessions (`POST /v1/connections`, `POST + // /v1/connections/{id}/reconnect`) carry a raw "{sessionId}.{nonce}" + // state — never JSON/base64-encoded — dispatched here before the legacy + // parse below, which would otherwise throw trying to atob/JSON.parse it. + // TODO(Phase 5): once every legacy JSON-state caller (builder pickers, + // ads/lead-ads/meta-catalog connect flows) moves onto sessions, this + // early branch becomes the only path and the switch below is deleted. + if (CONNECT_SESSION_STATE_PATTERN.test(rawStateParam)) { + return await handleConnectSessionCallback(url, rawStateParam) + } + let rawState: unknown try { - rawState = JSON.parse( - atob(decodeURIComponent(url.searchParams.get("state") || "")), - ) + rawState = JSON.parse(atob(decodeURIComponent(rawStateParam))) } catch { logger.debug( { url: url.toString() }, @@ -494,44 +592,14 @@ export const handleCallback = async ( return redirect(buildReconnectRedirectUrl(safeReferer, result)) } - const shortLivedToken = await exchangeMessengerCode( - messengerCredential.config, - code, - callbackUrl, - ) - // Exchange for a long-lived user token before the page-select step so - // the pending-auth cookie stays usable even when the user leaves the - // picker open for a long time. Best-effort: the short-lived token still - // covers the normal flow if the exchange fails. - const userToken = await exchangeMessengerLongLivedToken( - messengerCredential.config, - shortLivedToken, - ).catch((error) => { - logger.info( - { err: error }, - "Messenger long-lived token exchange failed, using short-lived token", - ) - return shortLivedToken - }) - const fbUser = await lookupFacebookUser(() => - getMessengerFacebookUser(userToken, messengerCredential.config.version), - ) - const token = await encryptAuth({ - userToken, - userId: fbUser?.id, - userName: fbUser?.name, - userAvatarUrl: fbUser?.avatarUrl, - workspaceId: workspace.id, - referer: safeReferer, - version: messengerCredential.config.version, - expiresAt: Date.now() + FB_PENDING_AUTH_MAX_AGE * 1000, - }) - - const cookieStore = await cookies() - writePendingAuth(cookieStore, FB_MESSENGER_PENDING_AUTH_COOKIE, token) - return redirect( - new URL("/channels/messenger/select", safeReferer).toString(), - ) + // A plain connect (no `flow`, no `reconnectIntegrationId`) never + // reaches here anymore: `channels/create/messenger/route.ts` mints a + // `ConnectSession` and its raw `"{sessionId}.{nonce}"` state is + // dispatched by `handleConnectSessionCallback` before this legacy + // switch ever runs. Every remaining caller of this callback sets one + // of `stateParams.flow` or `reconnectIntegrationId`, both handled + // above. + return notFound() } case "instagram": { @@ -577,18 +645,9 @@ export const handleCallback = async ( return redirect(buildReconnectRedirectUrl(safeReferer, result)) } - const token = await encryptAuth({ - userToken, - workspaceId: workspace.id, - referer: safeReferer, - version: instagramCredential.config.version, - expiresAt: Date.now() + FB_PENDING_AUTH_MAX_AGE * 1000, - }) - const cookieStore = await cookies() - writePendingAuth(cookieStore, FB_INSTAGRAM_PENDING_AUTH_COOKIE, token) - return redirect( - new URL("/channels/instagram/select", safeReferer).toString(), - ) + // A plain connect never reaches here anymore — see the comment at + // the end of `case "messenger"`. + return notFound() } case "instagramFacebook": { @@ -633,32 +692,9 @@ export const handleCallback = async ( return redirect(buildReconnectRedirectUrl(safeReferer, result)) } - const fbUser = await lookupFacebookUser(() => - getInstagramFacebookUser( - userToken, - instagramFacebookCredential.config.version, - ), - ) - - const token = await encryptAuth({ - userToken, - userId: fbUser?.id, - userName: fbUser?.name, - userAvatarUrl: fbUser?.avatarUrl, - workspaceId: workspace.id, - referer: safeReferer, - version: instagramFacebookCredential.config.version, - expiresAt: Date.now() + FB_PENDING_AUTH_MAX_AGE * 1000, - }) - const cookieStore = await cookies() - writePendingAuth( - cookieStore, - FB_INSTAGRAM_FACEBOOK_PENDING_AUTH_COOKIE, - token, - ) - return redirect( - new URL("/channels/instagram-facebook/select", safeReferer).toString(), - ) + // A plain connect never reaches here anymore — see the comment at + // the end of `case "messenger"`. + return notFound() } case "tiktok": { diff --git a/apps/builder/src/app/integrations/[...integration]/webhook.ts b/apps/builder/src/app/integrations/[...integration]/webhook.ts index ef438a227f..5ec0a7f3c2 100644 --- a/apps/builder/src/app/integrations/[...integration]/webhook.ts +++ b/apps/builder/src/app/integrations/[...integration]/webhook.ts @@ -1,4 +1,5 @@ import { + connectionStateService, customDomainService, platformCredentialService, resolveWorkspaceFreezeReason, @@ -6,9 +7,6 @@ import { userQuotaService, workspaceService, } from "@chatbotx.io/business" -import { db, eq } from "@chatbotx.io/database/client" -import { inboxStatuses } from "@chatbotx.io/database/partials" -import { inboxModel } from "@chatbotx.io/database/schema" import type { TiktokAuthValue, TiktokConfig, @@ -316,13 +314,14 @@ const handleTiktokWebhook = async (req: NextRequest) => { } if (eventType === "authorization.removed") { - await db - .update(inboxModel) - .set({ status: inboxStatuses.enum.disconnected }) - .where(eq(inboxModel.id, integrationTiktok.inboxId)) + await connectionStateService.markUnhealthyByIdentifier({ + provider: "tiktok", + identifier: userOpenId, + reason: "token_revoked", + }) logger.info( { openId: userOpenId }, - "TikTok authorization removed — inbox marked disconnected", + "TikTok authorization removed — connection marked unhealthy", ) return new Response("ok") } diff --git a/apps/builder/src/features/ads-campaign/actions/disconnect.action.ts b/apps/builder/src/features/ads-campaign/actions/disconnect.action.ts index 18466acf02..4976da1596 100644 --- a/apps/builder/src/features/ads-campaign/actions/disconnect.action.ts +++ b/apps/builder/src/features/ads-campaign/actions/disconnect.action.ts @@ -3,11 +3,11 @@ import { messagingAdsConnectionService } from "@chatbotx.io/business" import { workspaceIdAndIdRequestParams } from "@/features/common/schema" import { assertWorkspaceSuperAdmin } from "@/lib/auth/assert-workspace-super-admin" -import { workspaceActionClient } from "@/lib/safe-action" +import { workspaceActionClientAllowExpired } from "@/lib/safe-action" import { connectMessagingAdsRequest } from "./schema" /** `bindArgsParsedInputs`: `[workspaceId, integrationId]`. Best-effort revokes the Graph token, then deletes the `MessagingAdsConnection` row (and invalidates its cache) — see `messagingAdsConnectionService.revokeAndDisconnect`. */ -export const disconnectMessagingAdsAction = workspaceActionClient +export const disconnectMessagingAdsAction = workspaceActionClientAllowExpired .bindArgsSchemas(workspaceIdAndIdRequestParams) .inputSchema(connectMessagingAdsRequest) .action( diff --git a/apps/builder/src/features/ads-campaign/api/public.ts b/apps/builder/src/features/ads-campaign/api/public.ts index e1e92d8ee8..bbb13a42d3 100644 --- a/apps/builder/src/features/ads-campaign/api/public.ts +++ b/apps/builder/src/features/ads-campaign/api/public.ts @@ -432,6 +432,14 @@ export const adsCampaignPublicRouter = { } }), + /** + * NOT aliased to `connectionService.disconnect` — `MessagingAdsConnection` + * is a `sub_connection` (an attribute of a channel connection, keyed by + * `parentConnectionId`, not its own registry adapter). The plan's + * non-goals section reserves `sub_connection` as an enum value only for + * this rollout and explicitly keeps "Meta CAPI sub-connections and + * MessagingAdsConnection … their routes" out of scope. + */ disconnectConnection: workspaceTokenAuthAPI .route({ method: "DELETE", diff --git a/apps/builder/src/features/channel-connect/lib/registry.ts b/apps/builder/src/features/channel-connect/lib/registry.ts index 86ad58e1be..da923c0a4e 100644 --- a/apps/builder/src/features/channel-connect/lib/registry.ts +++ b/apps/builder/src/features/channel-connect/lib/registry.ts @@ -64,10 +64,16 @@ export type ConnectRoute = { // Arrow wrappers, never a bare `client.x.y` method reference: the oRPC // procedure client relies on its receiver, and a detached reference loses it // (AGENTS.md invariant 13). -const MESSENGER_CONNECT_ROUTE: ConnectRoute<{ pageId: string }> = { +const MESSENGER_CONNECT_ROUTE: ConnectRoute<{ + sessionId: string + pageId: string +}> = { call: (body) => client.integrationMessengerAPIs.connectMessengerPageAPI(body), } -const INSTAGRAM_FACEBOOK_CONNECT_ROUTE: ConnectRoute<{ igId: string }> = { +const INSTAGRAM_FACEBOOK_CONNECT_ROUTE: ConnectRoute<{ + sessionId: string + igId: string +}> = { call: (body) => client.integrationInstagramAPIs.connectInstagramFacebookAccountAPI(body), } @@ -81,7 +87,10 @@ const WHATSAPP_CONNECT_ROUTE: ConnectRoute = { * entry of its own — but its route belongs in this file with every other * channel literal. */ -export const INSTAGRAM_DIRECT_CONNECT_ROUTE: ConnectRoute<{ igId: string }> = { +export const INSTAGRAM_DIRECT_CONNECT_ROUTE: ConnectRoute<{ + sessionId: string + igId: string +}> = { call: (body) => client.integrationInstagramAPIs.connectInstagramAccountAPI(body), } diff --git a/apps/builder/src/features/channel-connect/lib/resolve-connect-session.ts b/apps/builder/src/features/channel-connect/lib/resolve-connect-session.ts index 69ff47da53..b791611376 100644 --- a/apps/builder/src/features/channel-connect/lib/resolve-connect-session.ts +++ b/apps/builder/src/features/channel-connect/lib/resolve-connect-session.ts @@ -6,6 +6,7 @@ import { workspaceMemberService, workspaceService, } from "@chatbotx.io/business" +import { connectSessionService } from "@chatbotx.io/business/connect-session" import { connectSessionExpiredException, credentialMissingException, @@ -15,16 +16,14 @@ import type { ChannelType, CredentialType, } from "@chatbotx.io/database/partials" -import type { WorkspaceModel } from "@chatbotx.io/database/types" +import type { + ConnectSessionModel, + WorkspaceModel, +} from "@chatbotx.io/database/types" import { BRANDING_TITLE, getBrandingUrl, } from "@/features/integration-webchat/lib" -import { - type FacebookAuthCallback, - readPendingAuth, -} from "@/lib/facebook-pending-auth" -import { resolvePlatformOwnerId } from "@/lib/platform-credential-owner" import { checkWorkspaceOwnerAccess, workspaceAccessDenialException, @@ -35,7 +34,7 @@ import { * by `packages/business` — derived here instead of widening every caller to * `unknown`. `NonNullable` drops the `undefined` branch: this module always * turns a missing credential into `credentialMissingException()` before - * returning, so callers never see it. + * returning. */ type ConnectCredential = NonNullable< Awaited>> @@ -50,13 +49,13 @@ type ConnectBrandingMenuEntry = { export type ConnectSessionRequest = { userId: string - cookieName: string + sessionId: string credentialType: T brandingChannel: ChannelType } export type ResolvedConnectSession = { - pendingAuth: FacebookAuthCallback + session: ConnectSessionModel workspace: WorkspaceModel platformOwnerId: string credential: ConnectCredential @@ -65,10 +64,18 @@ export type ResolvedConnectSession = { } /** - * Shared plan §2.4 steps 1–4: pending-auth cookie → workspace + membership → - * owner quota/trial gate → platform credential + branding menu entry. Every - * per-account connect action (Messenger today; Instagram's two actions in a - * later phase) starts here instead of re-implementing the same five checks. + * Shared per-account connect steps: `ConnectSession` row (workspace-unscoped + * lookup — the id is itself the capability token, same trust boundary as the + * `/connect/{id}` completion page) → workspace + membership → owner + * quota/trial gate → platform credential + branding menu entry. Every + * per-account connect action (Messenger, Instagram direct, Instagram-via- + * Facebook) starts here instead of re-implementing the same checks. + * + * Superseded the pending-auth-cookie version of this module: the session row + * already carries `workspaceId`/`provider`/`targets` (computed once by + * `ConnectionService.completeAuthorization`/`listAndAttachCandidates`), so + * there is no cookie to read and no live re-fetch of the provider's + * page/account list — `session.targets` is that list. * * Every failure throws one of the session-level exceptions * (`connectSessionExpiredException`, `notWorkspaceMemberException`, @@ -84,8 +91,8 @@ export type ResolvedConnectSession = { export async function resolveConnectSession( props: ConnectSessionRequest, ): Promise> { - const pendingAuth = await readPendingAuth(props.cookieName) - if (!pendingAuth) { + const session = await connectSessionService.findById(props.sessionId) + if (!session) { throw connectSessionExpiredException( "Your connect session expired. Please start again.", ) @@ -95,7 +102,7 @@ export async function resolveConnectSession( // "not a member of it" (a session error), not fall through to a generic // item-level `failed/unknown` outcome from an uncaught `notFoundException`. const workspace = await workspaceService.find({ - where: { id: pendingAuth.workspaceId }, + where: { id: session.workspaceId }, }) if (!workspace) { throw notWorkspaceMemberException() @@ -116,10 +123,12 @@ export async function resolveConnectSession( throw workspaceAccessDenialException(denialReason) } - const platformOwnerId = await resolvePlatformOwnerId({ - userId: props.userId, - workspaceId: workspace.id, - }) + const platformOwnerId = session.platformOwnerId + if (!platformOwnerId) { + throw credentialMissingException( + "App credentials are not configured for this workspace.", + ) + } const credential = await platformCredentialService.resolveForOwner({ ownerId: platformOwnerId, @@ -141,7 +150,7 @@ export async function resolveConnectSession( } return { - pendingAuth, + session, workspace, platformOwnerId, credential, diff --git a/apps/builder/src/features/connections/actions/disconnect-connection.action.ts b/apps/builder/src/features/connections/actions/disconnect-connection.action.ts new file mode 100644 index 0000000000..c173a40ca0 --- /dev/null +++ b/apps/builder/src/features/connections/actions/disconnect-connection.action.ts @@ -0,0 +1,32 @@ +"use server" + +import { connectionService } from "@chatbotx.io/connections" +import { zodBigintAsString } from "@chatbotx.io/utils" +import { workspaceActionClientAllowExpired } from "@/lib/safe-action" + +/** + * Generic disconnect for the unified `Connection` domain — the successor to + * the per-channel/per-provider `disconnect*.action.ts` files (messenger, + * whatsapp, active-campaign, …), on the same `workspaceActionClientAllowExpired` + * client so disconnect stays available once a workspace's trial has expired. + * + * Scope note: the ~24 existing per-provider disconnect actions are NOT + * converted to thin wrappers around this action. Several carry bespoke + * side effects this generic path deliberately does not replicate — the + * messenger/instagram shared-Facebook-page `general_info` preservation, + * WhatsApp's coexist/staging cleanup, per-provider audit-log strings, and + * `afterDisconnect` cache-invalidation hooks (e.g. the AI providers). Folding + * those into `ConnectionService.disconnect` without a live database to + * verify each one against would risk a silent regression in a + * quota/billing-critical path. They continue to call their own service's + * `disconnect` directly; this action is for new `Connection`-based UI only. + */ +export const disconnectConnectionAction = workspaceActionClientAllowExpired + .bindArgsSchemas([zodBigintAsString(), zodBigintAsString()]) + .action(async (props) => { + const { + bindArgsParsedInputs: [workspaceId, id], + } = props + + await connectionService.disconnect({ connectionId: id, workspaceId }) + }) diff --git a/apps/builder/src/features/connections/api/index.ts b/apps/builder/src/features/connections/api/index.ts new file mode 100644 index 0000000000..0eac91280f --- /dev/null +++ b/apps/builder/src/features/connections/api/index.ts @@ -0,0 +1 @@ +export { connectionsAPI, connectSessionsAPI } from "./private" diff --git a/apps/builder/src/features/connections/api/private.ts b/apps/builder/src/features/connections/api/private.ts new file mode 100644 index 0000000000..97438d0613 --- /dev/null +++ b/apps/builder/src/features/connections/api/private.ts @@ -0,0 +1,386 @@ +import { connectionStateService } from "@chatbotx.io/business" +import { connectSessionService } from "@chatbotx.io/business/connect-session" +import { + channelHiddenException, + connectionNotConfiguredException, + connectSessionExpiredException, + notFoundException, +} from "@chatbotx.io/business/errors" +import { + CONNECTION_REGISTRY, + connectionService, +} from "@chatbotx.io/connections" +import { withWorkspaceIdSchema } from "@/features/workspaces/schema/resource" +import { sanitizeOptionalReturnUrl } from "@/lib/oauth-referer" +import { resolvePlatformOwnerId } from "@/lib/platform-credential-owner" +import { resolveChannelPolicy } from "@/lib/workspace/resolve-visible-channels" +import { workspaceAuthorizedMidddleware } from "@/middlewares/auth" +import { authorizedAPI } from "@/orpc" +import { toConnectSessionResource } from "../lib/connect-session-resource" +import { resolveOAuthCredential } from "../lib/resolve-connect-credential" +import { + channelForProvider, + listConnectionProviderResources, + toConnectionResource, +} from "../lib/resolve-provider" +import { + connectSessionTargetsRequest, + createConnectionRequest, + getConnectionRequest, + getConnectSessionRequest, + listConnectionProvidersRequest, + listConnectionsRequest, + reconnectConnectionRequest, + submitConnectSessionInputRequest, + updateConnectionRequest, +} from "../schema/request" + +const listConnectionsAPI = authorizedAPI + .route({ + method: "GET", + path: "/workspaces/{workspaceId}/connections", + summary: "List connections", + tags: ["Connections"], + }) + .input(listConnectionsRequest.and(withWorkspaceIdSchema)) + .use(workspaceAuthorizedMidddleware, (input) => input.workspaceId) + .handler(async ({ input }) => { + const { data } = await connectionStateService.list({ + workspaceId: input.workspaceId, + kind: input.kind, + provider: input.provider, + channel: input.channel, + status: input.status ? [input.status] : undefined, + perPage: 50, + }) + return { data: data.map(toConnectionResource) } + }) + +const getConnectionAPI = authorizedAPI + .route({ + method: "GET", + path: "/workspaces/{workspaceId}/connections/{id}", + summary: "Get a connection", + tags: ["Connections"], + }) + .input(getConnectionRequest.and(withWorkspaceIdSchema)) + .use(workspaceAuthorizedMidddleware, (input) => input.workspaceId) + .handler(async ({ input }) => { + const connection = await connectionStateService.getForWorkspace({ + id: input.id, + workspaceId: input.workspaceId, + }) + if (!connection) { + throw notFoundException("Connection not found") + } + return toConnectionResource(connection) + }) + +const createConnectionAPI = authorizedAPI + .route({ + method: "POST", + path: "/workspaces/{workspaceId}/connections", + summary: "Connect a channel or integration", + tags: ["Connections"], + }) + .input(createConnectionRequest.and(withWorkspaceIdSchema)) + .use(workspaceAuthorizedMidddleware, (input) => input.workspaceId) + .handler(async ({ context, input }) => { + const adapter = CONNECTION_REGISTRY[input.provider] + if (!adapter) { + throw connectionNotConfiguredException(input.provider) + } + + const channel = channelForProvider(input.provider) + if (channel) { + const { data } = await connectionStateService.list({ + workspaceId: input.workspaceId, + provider: input.provider, + perPage: 1, + }) + if (data.length === 0) { + const policy = await resolveChannelPolicy(input.workspaceId) + if (policy && !policy.visibleChannels.includes(channel)) { + throw channelHiddenException(channel) + } + } + } + + const isCredentialStrategy = + adapter.provider.strategy === "token" || + adapter.provider.strategy === "api_key" || + adapter.provider.strategy === "self_serve" + + if (isCredentialStrategy) { + const connection = await connectionService.connectFromCredentials({ + workspaceId: input.workspaceId, + provider: input.provider, + config: input.config ?? {}, + actorUserId: context.user.id, + }) + return { connection: toConnectionResource(connection), session: null } + } + + const ownerId = await resolvePlatformOwnerId({ + userId: context.user.id, + workspaceId: input.workspaceId, + }) + const resolved = await resolveOAuthCredential({ + provider: input.provider, + ownerId, + }) + if (!resolved) { + throw connectionNotConfiguredException(input.provider) + } + const returnUrl = await sanitizeOptionalReturnUrl(input.redirectUrl) + const { session } = await connectionService.startSession({ + workspaceId: input.workspaceId, + provider: input.provider, + purpose: "connect", + credential: resolved.credential, + callbackUrl: resolved.callbackUrl, + actorUserId: context.user.id, + platformOwnerId: ownerId, + returnUrl, + }) + return { connection: null, session: toConnectSessionResource(session) } + }) + +const reconnectConnectionAPI = authorizedAPI + .route({ + method: "POST", + path: "/workspaces/{workspaceId}/connections/{id}/reconnect", + summary: "Re-authorize an existing connection", + tags: ["Connections"], + }) + .input(reconnectConnectionRequest.and(withWorkspaceIdSchema)) + .use(workspaceAuthorizedMidddleware, (input) => input.workspaceId) + .handler(async ({ context, input }) => { + const connection = await connectionStateService.getForWorkspace({ + id: input.id, + workspaceId: input.workspaceId, + }) + if (!connection) { + throw notFoundException("Connection not found") + } + const ownerId = await resolvePlatformOwnerId({ + userId: context.user.id, + workspaceId: input.workspaceId, + }) + const resolved = await resolveOAuthCredential({ + provider: connection.provider, + ownerId, + }) + if (!resolved) { + throw connectionNotConfiguredException(connection.provider) + } + const returnUrl = await sanitizeOptionalReturnUrl(input.redirectUrl) + const { session } = await connectionService.reconnect({ + connectionId: input.id, + workspaceId: input.workspaceId, + credential: resolved.credential, + callbackUrl: resolved.callbackUrl, + actorUserId: context.user.id, + platformOwnerId: ownerId, + returnUrl, + }) + return { connection: null, session: toConnectSessionResource(session) } + }) + +const updateConnectionAPI = authorizedAPI + .route({ + method: "PATCH", + path: "/workspaces/{workspaceId}/connections/{id}", + summary: "Rename a connection", + tags: ["Connections"], + }) + .input(updateConnectionRequest.and(withWorkspaceIdSchema)) + .use(workspaceAuthorizedMidddleware, (input) => input.workspaceId) + .handler(async ({ input }) => { + const connection = await connectionStateService.updateDisplayName({ + id: input.id, + workspaceId: input.workspaceId, + displayName: input.displayName, + }) + if (!connection) { + throw notFoundException("Connection not found") + } + return toConnectionResource(connection) + }) + +const disconnectConnectionAPI = authorizedAPI + .route({ + method: "DELETE", + path: "/workspaces/{workspaceId}/connections/{id}", + summary: "Disconnect a connection", + tags: ["Connections"], + }) + .input(getConnectionRequest.and(withWorkspaceIdSchema)) + .use(workspaceAuthorizedMidddleware, (input) => input.workspaceId) + .handler(async ({ input }) => { + const connection = await connectionService.disconnect({ + connectionId: input.id, + workspaceId: input.workspaceId, + }) + return toConnectionResource(connection) + }) + +const refreshConnectionAPI = authorizedAPI + .route({ + method: "POST", + path: "/workspaces/{workspaceId}/connections/{id}/refresh", + summary: "Force-refresh a connection's auth", + tags: ["Connections"], + }) + .input(getConnectionRequest.and(withWorkspaceIdSchema)) + .use(workspaceAuthorizedMidddleware, (input) => input.workspaceId) + .handler(async ({ input }) => { + const connection = await connectionService.refresh({ + connectionId: input.id, + workspaceId: input.workspaceId, + }) + return toConnectionResource(connection) + }) + +const verifyConnectionAPI = authorizedAPI + .route({ + method: "POST", + path: "/workspaces/{workspaceId}/connections/{id}/verify", + summary: "Run a live health check on a connection", + tags: ["Connections"], + }) + .input(getConnectionRequest.and(withWorkspaceIdSchema)) + .use(workspaceAuthorizedMidddleware, (input) => input.workspaceId) + .handler(async ({ input }) => { + const connection = await connectionService.verify({ + connectionId: input.id, + workspaceId: input.workspaceId, + }) + return toConnectionResource(connection) + }) + +const listConnectionProvidersAPI = authorizedAPI + .route({ + method: "GET", + path: "/workspaces/{workspaceId}/connection-providers", + summary: "List connection providers", + tags: ["Connections"], + }) + .input(listConnectionProvidersRequest.and(withWorkspaceIdSchema)) + .use(workspaceAuthorizedMidddleware, (input) => input.workspaceId) + .handler(async ({ input }) => { + // Support-session exemption from hidden-channel policy (the public + // route's `isSupportSession` parameter) is not yet threaded through this + // private path — deferred alongside the rest of the support-access + // integration this feature doesn't otherwise touch. + const data = await listConnectionProviderResources({ + workspaceId: input.workspaceId, + kind: input.kind, + }) + return { data } + }) + +const getConnectSessionAPI = authorizedAPI + .route({ + method: "GET", + path: "/workspaces/{workspaceId}/connect-sessions/{id}", + summary: "Get a connect session", + tags: ["Connections"], + }) + .input(getConnectSessionRequest.and(withWorkspaceIdSchema)) + .use(workspaceAuthorizedMidddleware, (input) => input.workspaceId) + .handler(async ({ input }) => { + const session = await connectSessionService.findByIdForWorkspace({ + id: input.id, + workspaceId: input.workspaceId, + }) + if (!session) { + throw notFoundException("Connect session not found") + } + return toConnectSessionResource(session) + }) + +const connectSessionTargetsAPI = authorizedAPI + .route({ + method: "POST", + path: "/workspaces/{workspaceId}/connect-sessions/{id}/targets", + summary: "Connect the selected targets of an awaiting_selection session", + tags: ["Connections"], + }) + .input(connectSessionTargetsRequest.and(withWorkspaceIdSchema)) + .use(workspaceAuthorizedMidddleware, (input) => input.workspaceId) + .handler(async ({ context, input }) => { + const result = await connectionService.connectTargets({ + sessionId: input.id, + workspaceId: input.workspaceId, + targetIds: input.targetIds, + actorUserId: context.user.id, + }) + return { + session: toConnectSessionResource(result.session), + connections: result.connections.map(toConnectionResource), + outcomes: result.outcomes, + } + }) + +const submitConnectSessionInputAPI = authorizedAPI + .route({ + method: "POST", + path: "/workspaces/{workspaceId}/connect-sessions/{id}/input", + summary: "Answer an enter_input step", + tags: ["Connections"], + }) + .input(submitConnectSessionInputRequest.and(withWorkspaceIdSchema)) + .use(workspaceAuthorizedMidddleware, (input) => input.workspaceId) + .handler(async ({ input }) => { + const session = await connectSessionService.requireByIdForWorkspace({ + id: input.id, + workspaceId: input.workspaceId, + }) + if (session.nextAction?.type !== "enter_input") { + throw connectSessionExpiredException( + "This connect session is not awaiting input.", + ) + } + const updated = await connectSessionService.submitInput({ + id: session.id, + nextAction: { type: "wait" }, + }) + return toConnectSessionResource(updated) + }) + +const cancelConnectSessionAPI = authorizedAPI + .route({ + method: "DELETE", + path: "/workspaces/{workspaceId}/connect-sessions/{id}", + summary: "Cancel a connect session", + tags: ["Connections"], + }) + .input(getConnectSessionRequest.and(withWorkspaceIdSchema)) + .use(workspaceAuthorizedMidddleware, (input) => input.workspaceId) + .handler(async ({ input }) => { + const session = await connectSessionService.cancel({ + id: input.id, + workspaceId: input.workspaceId, + }) + return toConnectSessionResource(session) + }) + +export const connectSessionsAPI = { + getConnectSessionAPI, + connectSessionTargetsAPI, + submitConnectSessionInputAPI, + cancelConnectSessionAPI, +} + +export const connectionsAPI = { + listConnectionsAPI, + getConnectionAPI, + createConnectionAPI, + reconnectConnectionAPI, + updateConnectionAPI, + disconnectConnectionAPI, + refreshConnectionAPI, + verifyConnectionAPI, + listConnectionProvidersAPI, +} diff --git a/apps/builder/src/features/connections/api/public.ts b/apps/builder/src/features/connections/api/public.ts new file mode 100644 index 0000000000..0ae678f425 --- /dev/null +++ b/apps/builder/src/features/connections/api/public.ts @@ -0,0 +1,428 @@ +import { connectionStateService } from "@chatbotx.io/business" +import { connectSessionService } from "@chatbotx.io/business/connect-session" +import { + channelHiddenException, + connectionNotConfiguredException, + connectSessionExpiredException, + notFoundException, +} from "@chatbotx.io/business/errors" +import { + CONNECTION_REGISTRY, + connectionService, +} from "@chatbotx.io/connections" +import { sanitizeOptionalReturnUrl } from "@/lib/oauth-referer" +import { + possibleErrorsOnCancelingConnectSession, + possibleErrorsOnConnectingSessionTargets, + possibleErrorsOnCreatingConnection, + possibleErrorsOnDisconnectingConnection, + possibleErrorsOnFindingConnectSession, + possibleErrorsOnFindingResource, + possibleErrorsOnListingResource, + possibleErrorsOnReconnectingConnection, + possibleErrorsOnRefreshingConnection, + possibleErrorsOnSubmittingConnectSessionInput, + possibleErrorsOnUpdatingConnection, + possibleErrorsOnVerifyingConnection, +} from "@/lib/orpc/orpc-error-helper" +import { resolveOwnerForWorkspace } from "@/lib/platform-credential-owner" +import { publicListResponse, withPublicPaging } from "@/lib/public-api/list" +import { resolveChannelPolicy } from "@/lib/workspace/resolve-visible-channels" +import { workspaceTokenAuthAPIForScope } from "@/orpc" +import { toConnectSessionResource } from "../lib/connect-session-resource" +import { resolveOAuthCredential } from "../lib/resolve-connect-credential" +import { + channelForProvider, + listConnectionProviderResources, + toConnectionResource, +} from "../lib/resolve-provider" +import { + connectSessionTargetsRequest, + createConnectionRequest, + getConnectionRequest, + getConnectSessionRequest, + listConnectionProvidersRequest, + listConnectionsRequest, + reconnectConnectionRequest, + submitConnectSessionInputRequest, + updateConnectionRequest, +} from "../schema/request" +import { + connectEnvelope, + connectionProviderResource, + connectionResource, + connectSessionResource, + connectSessionTargetsResource, +} from "../schema/resource" + +const workspaceTokenAuthAPI = workspaceTokenAuthAPIForScope("connections") + +export const connectionsPublicRouter = { + list: workspaceTokenAuthAPI + .route({ + method: "GET", + path: "/v1/connections", + summary: "List connections", + description: + "Every channel and integration connection in the workspace — the unified successor to /v1/integrations and the channel-specific list endpoints. Ordered by kind, then provider, then display name.", + tags: ["Connections"], + }) + .input(withPublicPaging(listConnectionsRequest)) + .output(publicListResponse(connectionResource)) + .errors(possibleErrorsOnListingResource) + .handler(async ({ context, input }) => { + const { data, count } = await connectionStateService.list({ + workspaceId: context.workspace.id, + kind: input.kind, + provider: input.provider, + channel: input.channel, + status: input.status ? [input.status] : undefined, + page: input.page, + perPage: input.perPage, + }) + return { + data: data.map(toConnectionResource), + pageCount: Math.max(1, Math.ceil(count / input.perPage)), + } + }), + + get: workspaceTokenAuthAPI + .route({ + method: "GET", + path: "/v1/connections/{id}", + summary: "Get a connection", + tags: ["Connections"], + }) + .input(getConnectionRequest) + .output(connectionResource) + .errors(possibleErrorsOnFindingResource) + .handler(async ({ context, input }) => { + const connection = await connectionStateService.getForWorkspace({ + id: input.id, + workspaceId: context.workspace.id, + }) + if (!connection) { + throw notFoundException("Connection not found") + } + return toConnectionResource(connection) + }), + + create: workspaceTokenAuthAPI + .route({ + method: "POST", + path: "/v1/connections", + successStatus: 201, + summary: "Connect a channel or integration", + description: + 'Credential-strategy providers (`token`/`api_key`/`self_serve`) connect immediately and return `connection` (`session: null`). OAuth providers (`oauth_redirect`/`oauth_popup`) return `connection: null` and a `session` whose `nextAction` is `{type:"open_url", url}` — show that URL to the person connecting, then poll `GET /v1/connect-sessions/{id}` until `awaiting_selection` (a multi-account provider) or `completed` (a single-account provider), then call `POST /v1/connect-sessions/{id}/targets` to finish a multi-account connect. The person opening the URL must have admin rights on the account being connected: the resulting token belongs to them, so hand each person their own link — a leaked URL can only connect the opener\'s account into this workspace.', + tags: ["Connections"], + }) + .input(createConnectionRequest) + .output(connectEnvelope) + .errors(possibleErrorsOnCreatingConnection) + .handler(async ({ context, input }) => { + const adapter = CONNECTION_REGISTRY[input.provider] + if (!adapter) { + throw connectionNotConfiguredException(input.provider) + } + + const channel = channelForProvider(input.provider) + if (channel) { + const { data } = await connectionStateService.list({ + workspaceId: context.workspace.id, + provider: input.provider, + perPage: 1, + }) + if (data.length === 0) { + const policy = await resolveChannelPolicy(context.workspace.id) + if (policy && !policy.visibleChannels.includes(channel)) { + throw channelHiddenException(channel) + } + } + } + + const isCredentialStrategy = + adapter.provider.strategy === "token" || + adapter.provider.strategy === "api_key" || + adapter.provider.strategy === "self_serve" + + if (isCredentialStrategy) { + const connection = await connectionService.connectFromCredentials({ + workspaceId: context.workspace.id, + provider: input.provider, + config: input.config ?? {}, + }) + return { connection: toConnectionResource(connection), session: null } + } + + const ownerId = await resolveOwnerForWorkspace(context.workspace) + const resolved = await resolveOAuthCredential({ + provider: input.provider, + ownerId, + }) + if (!resolved) { + throw connectionNotConfiguredException(input.provider) + } + const returnUrl = await sanitizeOptionalReturnUrl(input.redirectUrl) + const { session } = await connectionService.startSession({ + workspaceId: context.workspace.id, + provider: input.provider, + purpose: "connect", + credential: resolved.credential, + callbackUrl: resolved.callbackUrl, + actorTokenId: context.apiToken.id, + platformOwnerId: ownerId, + returnUrl, + }) + return { connection: null, session: toConnectSessionResource(session) } + }), + + reconnect: workspaceTokenAuthAPI + .route({ + method: "POST", + path: "/v1/connections/{id}/reconnect", + successStatus: 201, + summary: "Re-authorize an existing connection", + description: + "Starts a new OAuth authorization for this exact connection (typically after it went `needs_reauth`). The re-granted account must match the one being reconnected, or the attempt fails once the browser round trip completes. Same envelope as `POST /v1/connections`; `connection` is always `null` here — a reconnect never resolves without the round trip.", + tags: ["Connections"], + }) + .input(reconnectConnectionRequest) + .output(connectEnvelope) + .errors(possibleErrorsOnReconnectingConnection) + .handler(async ({ context, input }) => { + const connection = await connectionStateService.getForWorkspace({ + id: input.id, + workspaceId: context.workspace.id, + }) + if (!connection) { + throw notFoundException("Connection not found") + } + const ownerId = await resolveOwnerForWorkspace(context.workspace) + const resolved = await resolveOAuthCredential({ + provider: connection.provider, + ownerId, + }) + if (!resolved) { + throw connectionNotConfiguredException(connection.provider) + } + const returnUrl = await sanitizeOptionalReturnUrl(input.redirectUrl) + const { session } = await connectionService.reconnect({ + connectionId: input.id, + workspaceId: context.workspace.id, + credential: resolved.credential, + callbackUrl: resolved.callbackUrl, + actorTokenId: context.apiToken.id, + platformOwnerId: ownerId, + returnUrl, + }) + return { connection: null, session: toConnectSessionResource(session) } + }), + + update: workspaceTokenAuthAPI + .route({ + method: "PATCH", + path: "/v1/connections/{id}", + summary: "Rename a connection", + description: + "Updates the connection's display name only — provider configuration stays on provider-specific routes.", + tags: ["Connections"], + }) + .input(updateConnectionRequest) + .output(connectionResource) + .errors(possibleErrorsOnUpdatingConnection) + .handler(async ({ context, input }) => { + const connection = await connectionStateService.updateDisplayName({ + id: input.id, + workspaceId: context.workspace.id, + displayName: input.displayName, + }) + if (!connection) { + throw notFoundException("Connection not found") + } + return toConnectionResource(connection) + }), + + disconnect: workspaceTokenAuthAPI + .route({ + method: "DELETE", + path: "/v1/connections/{id}", + summary: "Disconnect a connection", + description: + "Best-effort provider-side teardown (revoke/unsubscribe), then marks the connection disconnected. Never fails the local disconnect on an upstream API error.", + tags: ["Connections"], + }) + .input(getConnectionRequest) + .output(connectionResource) + .errors(possibleErrorsOnDisconnectingConnection) + .handler(async ({ context, input }) => { + const connection = await connectionService.disconnect({ + connectionId: input.id, + workspaceId: context.workspace.id, + }) + return toConnectionResource(connection) + }), + + refresh: workspaceTokenAuthAPI + .route({ + method: "POST", + path: "/v1/connections/{id}/refresh", + summary: "Force-refresh a connection's auth", + description: + "Calls the provider's token refresh regardless of expiry. 409 if the connection is not active; 400 if the provider does not support refresh.", + tags: ["Connections"], + }) + .input(getConnectionRequest) + .output(connectionResource) + .errors(possibleErrorsOnRefreshingConnection) + .handler(async ({ context, input }) => { + const connection = await connectionService.refresh({ + connectionId: input.id, + workspaceId: context.workspace.id, + }) + return toConnectionResource(connection) + }), + + verify: workspaceTokenAuthAPI + .route({ + method: "POST", + path: "/v1/connections/{id}/verify", + summary: "Run a live health check on a connection", + description: + "Calls the provider's health check without refreshing auth, and updates the connection's status from the result. 409 if the connection is not active.", + tags: ["Connections"], + }) + .input(getConnectionRequest) + .output(connectionResource) + .errors(possibleErrorsOnVerifyingConnection) + .handler(async ({ context, input }) => { + const connection = await connectionService.verify({ + connectionId: input.id, + workspaceId: context.workspace.id, + }) + return toConnectionResource(connection) + }), +} + +export const connectionProvidersPublicRouter = { + list: workspaceTokenAuthAPI + .route({ + method: "GET", + path: "/v1/connection-providers", + summary: "List connection providers", + description: + "The full connect catalog — every provider's strategy, config fields, and whether this workspace can connect it right now. Use `configFields` to build the `config` object for `POST /v1/connections`.", + tags: ["Connections"], + }) + .input(listConnectionProvidersRequest) + .output(publicListResponse(connectionProviderResource)) + .errors(possibleErrorsOnListingResource) + .handler(async ({ context, input }) => { + const data = await listConnectionProviderResources({ + workspaceId: context.workspace.id, + kind: input.kind, + }) + return { data, pageCount: 1 } + }), +} + +export const connectSessionsPublicRouter = { + get: workspaceTokenAuthAPI + .route({ + method: "GET", + path: "/v1/connect-sessions/{id}", + summary: "Get a connect session", + description: + "Poll this after `POST /v1/connections`/`POST /v1/connections/{id}/reconnect` returns a `session`. `status` moves `pending` -> `awaiting_selection` (multi-account) or straight to `completed` (single-account) once the OAuth round trip finishes, or `failed`/`expired`/`cancelled`. Clients must tolerate unknown future values in `status`/`nextAction.type`.", + tags: ["Connections"], + }) + .input(getConnectSessionRequest) + .output(connectSessionResource) + .errors(possibleErrorsOnFindingConnectSession) + .handler(async ({ context, input }) => { + const session = await connectSessionService.findByIdForWorkspace({ + id: input.id, + workspaceId: context.workspace.id, + }) + if (!session) { + throw notFoundException("Connect session not found") + } + return toConnectSessionResource(session) + }), + + connectTargets: workspaceTokenAuthAPI + .route({ + method: "POST", + path: "/v1/connect-sessions/{id}/targets", + summary: "Connect the selected targets of an awaiting_selection session", + description: + "Finishes a multi-account OAuth connect: claims and connects each requested target, one outcome per target (`connected`/`duplicated`/`limitReached`/`failed` — never throws for a single target's failure). 400 if the session is not `awaiting_selection`.", + tags: ["Connections"], + }) + .input(connectSessionTargetsRequest) + .output(connectSessionTargetsResource) + .errors(possibleErrorsOnConnectingSessionTargets) + .handler(async ({ context, input }) => { + const result = await connectionService.connectTargets({ + sessionId: input.id, + workspaceId: context.workspace.id, + targetIds: input.targetIds, + }) + return { + session: toConnectSessionResource(result.session), + connections: result.connections.map(toConnectionResource), + outcomes: result.outcomes, + } + }), + + submitInput: workspaceTokenAuthAPI + .route({ + method: "POST", + path: "/v1/connect-sessions/{id}/input", + summary: "Answer an enter_input step", + description: + "Answers a session whose `nextAction.type` is `enter_input` (e.g. a future WhatsApp registration PIN step). No v1 provider produces an `enter_input` step yet — this route ships so clients are already written against the full protocol. 400 if the session is not currently awaiting input.", + tags: ["Connections"], + }) + .input(submitConnectSessionInputRequest) + .output(connectSessionResource) + .errors(possibleErrorsOnSubmittingConnectSessionInput) + .handler(async ({ context, input }) => { + const session = await connectSessionService.requireByIdForWorkspace({ + id: input.id, + workspaceId: context.workspace.id, + }) + if (session.nextAction?.type !== "enter_input") { + throw connectSessionExpiredException( + "This connect session is not awaiting input.", + ) + } + // No v1 provider strategy defines an `enter_input` handler yet, so + // this path never runs in practice today (`nextAction.type` is + // never `enter_input` above) — kept minimal (acknowledge, then wait) + // so the shape is stable for whichever future strategy needs it. + const updated = await connectSessionService.submitInput({ + id: session.id, + nextAction: { type: "wait" }, + }) + return toConnectSessionResource(updated) + }), + + cancel: workspaceTokenAuthAPI + .route({ + method: "DELETE", + path: "/v1/connect-sessions/{id}", + summary: "Cancel a connect session", + tags: ["Connections"], + }) + .input(getConnectSessionRequest) + .output(connectSessionResource) + .errors(possibleErrorsOnCancelingConnectSession) + .handler(async ({ context, input }) => { + const session = await connectSessionService.cancel({ + id: input.id, + workspaceId: context.workspace.id, + }) + return toConnectSessionResource(session) + }), +} diff --git a/apps/builder/src/features/connections/lib/connect-session-resource.ts b/apps/builder/src/features/connections/lib/connect-session-resource.ts new file mode 100644 index 0000000000..85b7bb4f86 --- /dev/null +++ b/apps/builder/src/features/connections/lib/connect-session-resource.ts @@ -0,0 +1,18 @@ +import type { ConnectSessionModel } from "@chatbotx.io/database/types" +import type { ConnectSessionResource } from "../schema/resource" + +/** Turns a `ConnectSession` row into its public/private DTO — never `encryptedAuth`/`claimedTargetIds`/`stateNonceHash`. */ +export const toConnectSessionResource = ( + row: ConnectSessionModel, +): ConnectSessionResource => ({ + id: row.id, + provider: row.provider, + purpose: row.purpose, + status: row.status, + step: row.step, + nextAction: row.nextAction ?? null, + targets: row.targets, + connectionIds: row.resultConnectionIds, + errorCode: row.errorCode ?? null, + expiresAt: row.expiresAt.toISOString(), +}) diff --git a/apps/builder/src/features/connections/lib/resolve-connect-credential.ts b/apps/builder/src/features/connections/lib/resolve-connect-credential.ts new file mode 100644 index 0000000000..9322687fad --- /dev/null +++ b/apps/builder/src/features/connections/lib/resolve-connect-credential.ts @@ -0,0 +1,69 @@ +import "server-only" + +import { platformCredentialService } from "@chatbotx.io/business" +import { CONNECTION_REGISTRY } from "@chatbotx.io/connections" +import type { IntegrationType } from "@chatbotx.io/database/partials" +import { buildProviderCallbackUrl } from "@/lib/provider-origin" + +/** + * The registered OAuth `redirect_uri` path per provider — matches the + * existing per-provider `case` branches in + * `apps/builder/src/app/integrations/[...integration]/callback.ts` exactly. + * Most `IntegrationType` keys are already the callback slug (`messenger`, + * `instagram`, `tiktok`, `zalo`); four are camelCase registry keys with a + * kebab-case callback path already registered with the provider (changing + * either would need a new `redirect_uri` allow-listed with each provider, + * so this map — not a generic `camelCase -> kebab-case` transform — is the + * source of truth). + */ +const OAUTH_CALLBACK_SLUG: Partial> = { + instagramFacebook: "instagram-facebook", + facebookAds: "facebook-ads", + googleCalendar: "google-calendar", + googleSheets: "google-sheets", +} + +/** + * Resolves the tenant-aware platform credential + callback URL an OAuth + * connect (`startSession`/`reconnect`) needs, for whichever `IntegrationType` + * the caller wants to connect. `ownerId` is pre-resolved by the caller — + * `resolveOwnerForWorkspace` for a workspace-token request (no interactive + * user), `resolvePlatformOwnerId` for a signed-in builder session — so this + * helper stays auth-context-agnostic and is shared by the public and private + * routers. + * + * Returns `null` when the provider isn't OAuth-configured (no + * `credentialType` registered, e.g. a credential-strategy or built-in + * provider) or the owner has no credential configured for it yet. + */ +export async function resolveOAuthCredential(input: { + provider: IntegrationType + ownerId: string +}): Promise<{ + credential: Record + callbackUrl: string +} | null> { + const adapter = CONNECTION_REGISTRY[input.provider] + if (!adapter?.credentialType) { + return null + } + const resolved = await platformCredentialService.resolveForOwner({ + ownerId: input.ownerId, + type: adapter.credentialType, + }) + if (!resolved) { + return null + } + const slug = OAUTH_CALLBACK_SLUG[input.provider] ?? input.provider + const callbackUrl = await buildProviderCallbackUrl( + resolved, + `/integrations/${slug}/callback`, + ) + return { + // `CredentialByType[T]` is provider-specific and this call site is + // deliberately generic across every `IntegrationType` — same erasure + // `ConnectionCredential = unknown` already models on the SDK side. + credential: resolved.config as Record, + callbackUrl, + } +} diff --git a/apps/builder/src/features/connections/lib/resolve-provider.ts b/apps/builder/src/features/connections/lib/resolve-provider.ts new file mode 100644 index 0000000000..e96055698b --- /dev/null +++ b/apps/builder/src/features/connections/lib/resolve-provider.ts @@ -0,0 +1,192 @@ +import { + connectionStateService, + platformCredentialService, +} from "@chatbotx.io/business" +import { CONNECTION_REGISTRY } from "@chatbotx.io/connections" +import type { + ChannelType, + IntegrationType, +} from "@chatbotx.io/database/partials" +import { integrationTypes } from "@chatbotx.io/database/partials" +import type { ConnectionModel } from "@chatbotx.io/database/types" +import { getTranslations } from "next-intl/server" +import { resolveChannelPolicy } from "@/lib/workspace/resolve-visible-channels" +import type { + ConnectionProviderResource, + ConnectionResource, +} from "../schema/resource" + +/** Turns a `Connection` row into its public/private DTO — `capabilities` is joined from `CONNECTION_REGISTRY`, never stored on the row. */ +export const toConnectionResource = ( + row: ConnectionModel, +): ConnectionResource => { + const adapter = CONNECTION_REGISTRY[row.provider] + return { + id: row.id, + kind: row.kind, + provider: row.provider, + channel: row.channel, + status: row.status, + statusReason: row.statusReason, + sourceId: row.sourceId, + displayName: row.displayName, + inboxId: row.inboxId, + integrationId: row.integrationId, + strategy: adapter?.provider.strategy ?? "self_serve", + capabilities: { + refreshable: Boolean(adapter?.integration?.refreshAuth), + verifiable: Boolean(adapter?.provider.verify), + multiAccount: adapter?.provider.multiAccount ?? false, + }, + authExpiresAt: row.authExpiresAt?.toISOString() ?? null, + lastError: row.lastError, + connectedAt: row.connectedAt?.toISOString() ?? null, + disconnectedAt: row.disconnectedAt?.toISOString() ?? null, + createdAt: row.createdAt.toISOString(), + updatedAt: row.updatedAt.toISOString(), + } +} + +/** + * Builds the `GET /v1/connection-providers` catalog: every `IntegrationType`, + * with `available`/`unavailableReason` resolved against this workspace + * (hidden-channel policy, an existing single-account connection, or a + * missing platform credential). `resolveChannelPolicy`'s `ownerId` is the + * tenant-aware credential owner for every provider, not just channels — it's + * reused here rather than a second `resolveOwnerForWorkspace` read. + */ +export const listConnectionProviderResources = async (input: { + workspaceId: string + kind?: "channel" | "integration" | "sub_connection" + isSupportSession?: boolean +}): Promise => { + const [t, policy] = await Promise.all([ + getTranslations(), + resolveChannelPolicy(input.workspaceId), + ]) + + const alreadyConnectedByProvider = await resolveAlreadyConnectedProviders( + input.workspaceId, + ) + + const resources = await Promise.all( + integrationTypes.options + .filter( + (provider) => + !input.kind || + CONNECTION_REGISTRY[provider]?.provider.kind === input.kind, + ) + .map((provider) => + resolveOneProvider({ + provider, + policy, + isSupportSession: input.isSupportSession ?? false, + alreadyConnected: alreadyConnectedByProvider.has(provider), + t, + }), + ), + ) + return resources.filter( + (resource): resource is ConnectionProviderResource => resource !== null, + ) +} + +const resolveAlreadyConnectedProviders = async ( + workspaceId: string, +): Promise> => { + // Only distinct `provider` values matter here (deduped into the `Set` + // below); 50 is the DB-level page-size cap (`maxLimit`) — comfortably + // above the 30-value `IntegrationType` domain this loop iterates. + const { data } = await connectionStateService.list({ + workspaceId, + perPage: 50, + }) + return new Set(data.map((row) => row.provider)) +} + +const resolveOneProvider = async (input: { + provider: IntegrationType + policy: Awaited> + isSupportSession: boolean + alreadyConnected: boolean + t: Awaited> +}): Promise => { + const adapter = CONNECTION_REGISTRY[input.provider] + if (!adapter) { + return null + } + + const configFields = adapter.provider.configFields.map((field) => ({ + name: field.name, + type: field.type, + required: field.required, + label: input.t.has(field.labelKey) ? input.t(field.labelKey) : field.name, + enumValues: field.enumValues ? [...field.enumValues] : undefined, + description: field.description, + })) + + const unavailableReason = await resolveUnavailableReason(input) + + return { + provider: input.provider, + kind: adapter.provider.kind, + channel: (adapter.provider.kind === "channel" + ? (channelForProvider(input.provider) ?? null) + : null) as ChannelType | null, + strategy: adapter.provider.strategy, + multiAccount: adapter.provider.multiAccount, + configFields, + available: unavailableReason === null, + unavailableReason, + } +} + +export const channelForProvider = ( + provider: IntegrationType, +): ChannelType | undefined => { + const adapter = CONNECTION_REGISTRY[provider] + if (adapter?.provider.kind !== "channel") { + return + } + return provider === "instagramFacebook" + ? "instagram" + : (provider as ChannelType) +} + +const resolveUnavailableReason = async (input: { + provider: IntegrationType + policy: Awaited> + isSupportSession: boolean + alreadyConnected: boolean +}): Promise => { + const adapter = CONNECTION_REGISTRY[input.provider] + if (!adapter) { + return "notImplemented" + } + + if (!(adapter.provider.multiAccount || !input.alreadyConnected)) { + return "alreadyConnected" + } + + const channel = channelForProvider(input.provider) + if ( + channel && + input.policy && + !input.isSupportSession && + !input.policy.visibleChannels.includes(channel) + ) { + return "hiddenForTenant" + } + + if (adapter.credentialType && input.policy) { + const credential = await platformCredentialService.resolveForOwner({ + ownerId: input.policy.ownerId, + type: adapter.credentialType, + }) + if (!credential) { + return "credentialMissing" + } + } + + return null +} diff --git a/apps/builder/src/features/connections/schema/request.ts b/apps/builder/src/features/connections/schema/request.ts new file mode 100644 index 0000000000..a2d1fb803b --- /dev/null +++ b/apps/builder/src/features/connections/schema/request.ts @@ -0,0 +1,53 @@ +import { + channelTypes, + connectionKinds, + connectionStatuses, + integrationTypes, +} from "@chatbotx.io/database/partials" +import { z } from "zod" + +export const listConnectionsRequest = z.object({ + kind: connectionKinds.optional(), + provider: integrationTypes.optional(), + channel: channelTypes.optional(), + status: connectionStatuses.optional(), +}) + +export const getConnectionRequest = z.object({ + id: z.string(), +}) + +export const listConnectionProvidersRequest = z.object({ + kind: connectionKinds.optional(), +}) + +export const createConnectionRequest = z.object({ + provider: integrationTypes, + config: z.record(z.string(), z.unknown()).optional(), + /** Where to send the browser once an OAuth connect session finishes (validated with `sanitizeReferer`). Ignored for a credential-strategy connect. */ + redirectUrl: z.url().optional(), +}) + +export const reconnectConnectionRequest = z.object({ + id: z.string(), + redirectUrl: z.url().optional(), +}) + +export const updateConnectionRequest = z.object({ + id: z.string(), + displayName: z.string().trim().min(1).max(200), +}) + +export const getConnectSessionRequest = z.object({ + id: z.string(), +}) + +export const connectSessionTargetsRequest = z.object({ + id: z.string(), + targetIds: z.array(z.string()).min(1), +}) + +export const submitConnectSessionInputRequest = z.object({ + id: z.string(), + input: z.record(z.string(), z.unknown()), +}) diff --git a/apps/builder/src/features/connections/schema/resource.ts b/apps/builder/src/features/connections/schema/resource.ts new file mode 100644 index 0000000000..1abc23a37a --- /dev/null +++ b/apps/builder/src/features/connections/schema/resource.ts @@ -0,0 +1,114 @@ +import { + connectionKinds, + connectionStatuses, + connectSessionNextActionSchema, + connectSessionOutcomeSchema, + connectSessionPurposes, + connectSessionStatuses, + connectSessionTargetSchema, +} from "@chatbotx.io/database/partials" +import { z } from "zod" + +/** + * Public/private `Connection` DTO — explicit field list, **never** `auth`. + * `capabilities` is joined from `CONNECTION_REGISTRY` at response time (see + * `lib/resolve-provider.ts`), not stored on the row. + */ +export const connectionResource = z.object({ + id: z.string(), + kind: connectionKinds, + provider: z.string(), + channel: z.string().nullable(), + status: connectionStatuses, + statusReason: z.string().nullable(), + sourceId: z.string(), + displayName: z.string(), + inboxId: z.string().nullable(), + integrationId: z.string().nullable(), + strategy: z.enum([ + "oauth_redirect", + "oauth_popup", + "token", + "api_key", + "self_serve", + ]), + capabilities: z.object({ + refreshable: z.boolean(), + verifiable: z.boolean(), + multiAccount: z.boolean(), + }), + authExpiresAt: z.string().nullable(), + lastError: z.string().nullable(), + connectedAt: z.string().nullable(), + disconnectedAt: z.string().nullable(), + createdAt: z.string(), + updatedAt: z.string(), +}) +export type ConnectionResource = z.infer + +export const connectionProviderConfigField = z.object({ + name: z.string(), + type: z.enum(["string", "secret", "number", "boolean", "enum", "url"]), + required: z.boolean(), + label: z.string(), + enumValues: z.array(z.string()).optional(), + description: z.string().optional(), +}) + +export const connectionProviderResource = z.object({ + provider: z.string(), + kind: connectionKinds, + channel: z.string().nullable(), + strategy: z.enum([ + "oauth_redirect", + "oauth_popup", + "token", + "api_key", + "self_serve", + ]), + multiAccount: z.boolean(), + configFields: z.array(connectionProviderConfigField), + available: z.boolean(), + unavailableReason: z + .enum([ + "notImplemented", + "hiddenForTenant", + "alreadyConnected", + "credentialMissing", + ]) + .nullable(), +}) +export type ConnectionProviderResource = z.infer< + typeof connectionProviderResource +> + +/** `ConnectSession` DTO — `GET /v1/connect-sessions/{id}` and the connect envelope's `session` field. Never `encryptedAuth`/`claimedTargetIds`/`stateNonceHash`. */ +export const connectSessionResource = z.object({ + id: z.string(), + provider: z.string(), + purpose: connectSessionPurposes, + status: connectSessionStatuses, + step: z.string(), + nextAction: connectSessionNextActionSchema.nullable(), + targets: z.array(connectSessionTargetSchema), + connectionIds: z.array(z.string()), + errorCode: z.string().nullable(), + expiresAt: z.string(), +}) +export type ConnectSessionResource = z.infer + +/** `POST /v1/connections` and `POST /v1/connections/{id}/reconnect` response envelope: exactly one of `connection`/`session` is non-null. */ +export const connectEnvelope = z.object({ + connection: connectionResource.nullable(), + session: connectSessionResource.nullable(), +}) +export type ConnectEnvelope = z.infer + +/** `POST /v1/connect-sessions/{id}/targets` response. */ +export const connectSessionOutcomeResource = connectSessionOutcomeSchema + +export const connectSessionTargetsResource = z.object({ + session: connectSessionResource, + connections: z.array(connectionResource), + outcomes: z.array(connectSessionOutcomeResource), +}) diff --git a/apps/builder/src/features/external-calendars/actions/disconnect.action.ts b/apps/builder/src/features/external-calendars/actions/disconnect.action.ts index 7d9ae9d287..97cc21f1d7 100644 --- a/apps/builder/src/features/external-calendars/actions/disconnect.action.ts +++ b/apps/builder/src/features/external-calendars/actions/disconnect.action.ts @@ -2,9 +2,9 @@ import { appointmentExternalCalendarService } from "@chatbotx.io/business" import { workspaceIdAndIdRequestParams } from "@/features/common/schema" -import { workspaceActionClient } from "@/lib/safe-action" +import { workspaceActionClientAllowExpired } from "@/lib/safe-action" -export const disconnectGoogleCalendarAction = workspaceActionClient +export const disconnectGoogleCalendarAction = workspaceActionClientAllowExpired .bindArgsSchemas(workspaceIdAndIdRequestParams) .action(async ({ bindArgsParsedInputs: [workspaceId, integrationId] }) => { await appointmentExternalCalendarService.disconnect({ diff --git a/apps/builder/src/features/external-calendars/api/public.ts b/apps/builder/src/features/external-calendars/api/public.ts index 4977220f5d..c2aea0fab7 100644 --- a/apps/builder/src/features/external-calendars/api/public.ts +++ b/apps/builder/src/features/external-calendars/api/public.ts @@ -40,6 +40,17 @@ export const appointmentExternalCalendarsPublicRouter = { return paginateInMemory(items, input) }), + /** + * NOT aliased to `connectionService.disconnect` (unlike the AI-provider + * DELETE route) — `getDisconnectableGoogleConnection`'s `connectionInUse` + * 409 guard (any appointment calendar still referencing this connection) + * is a business rule the registry-only `ConnectionService` has no way to + * know about, and this endpoint is Google-only in practice + * (`outlookCalendar` has no `ConnectionAdapter` yet) — see + * `out/plan/…-integration-nested-ember.md`'s non-goals: "Meta CAPI + * sub-connections and MessagingAdsConnection … keep their routes" + * applies by the same reasoning to this provider-specific guard. + */ disconnect: workspaceTokenAuthAPI .route({ method: "DELETE", diff --git a/apps/builder/src/features/external-webhooks/api/public.ts b/apps/builder/src/features/external-webhooks/api/public.ts index ba64679ed4..987ce9c26e 100644 --- a/apps/builder/src/features/external-webhooks/api/public.ts +++ b/apps/builder/src/features/external-webhooks/api/public.ts @@ -13,7 +13,7 @@ import { } from "@/lib/public-api/list" import { workspaceTokenAuthAPIForScope } from "@/orpc" -const workspaceTokenAuthAPI = workspaceTokenAuthAPIForScope("integrations") +const workspaceTokenAuthAPI = workspaceTokenAuthAPIForScope("connections") const externalWebhookResource = z.object({ id: z.string(), diff --git a/apps/builder/src/features/integration-active-campaign/actions/disconnect.action.ts b/apps/builder/src/features/integration-active-campaign/actions/disconnect.action.ts index 08e87185b3..7b64bd4b6e 100644 --- a/apps/builder/src/features/integration-active-campaign/actions/disconnect.action.ts +++ b/apps/builder/src/features/integration-active-campaign/actions/disconnect.action.ts @@ -5,5 +5,5 @@ import { createDisconnectAction } from "@/lib/integration-actions" export const disconnectActiveCampaignAction = createDisconnectAction( integrationActiveCampaignService, - { name: "ActiveCampaign" }, + { name: "ActiveCampaign", provider: "activeCampaign" }, ) diff --git a/apps/builder/src/features/integration-claude/actions/disconnect.action.ts b/apps/builder/src/features/integration-claude/actions/disconnect.action.ts index 0add63b53a..76613c729d 100644 --- a/apps/builder/src/features/integration-claude/actions/disconnect.action.ts +++ b/apps/builder/src/features/integration-claude/actions/disconnect.action.ts @@ -9,6 +9,7 @@ export const disconnectClaudeAction = createDisconnectAction( integrationClaudeService, { name: "Claude", + provider: "claude", log: false, afterDisconnect: async (workspaceId) => { await aiIntegrationService.invalidateCache( diff --git a/apps/builder/src/features/integration-claude/lib/index.ts b/apps/builder/src/features/integration-claude/lib/index.ts index c09b260e1e..b7e2e0e4ac 100644 --- a/apps/builder/src/features/integration-claude/lib/index.ts +++ b/apps/builder/src/features/integration-claude/lib/index.ts @@ -1,5 +1,4 @@ -import { aiProviders } from "@chatbotx.io/ai" -import { verifyAiProviderApiKey } from "@/features/integration-ai/lib/verify-api-key" +import { verifyAiProviderApiKey } from "@chatbotx.io/business/integration-ai-provider/verify" export const verifyClaudeApiKey = (apiKey: string) => - verifyAiProviderApiKey(aiProviders.enum.claude, apiKey) + verifyAiProviderApiKey("claude", apiKey) diff --git a/apps/builder/src/features/integration-deepseek/actions/disconnect.action.ts b/apps/builder/src/features/integration-deepseek/actions/disconnect.action.ts index 28226d9892..1f3b485af3 100644 --- a/apps/builder/src/features/integration-deepseek/actions/disconnect.action.ts +++ b/apps/builder/src/features/integration-deepseek/actions/disconnect.action.ts @@ -9,6 +9,7 @@ export const disconnectDeepSeekAction = createDisconnectAction( integrationDeepSeekService, { name: "DeepSeek", + provider: "deepseek", log: false, afterDisconnect: async (workspaceId) => { await aiIntegrationService.invalidateCache( diff --git a/apps/builder/src/features/integration-deepseek/lib/index.ts b/apps/builder/src/features/integration-deepseek/lib/index.ts index 4772ddf8be..89e1c625e7 100644 --- a/apps/builder/src/features/integration-deepseek/lib/index.ts +++ b/apps/builder/src/features/integration-deepseek/lib/index.ts @@ -1,5 +1,4 @@ -import { aiProviders } from "@chatbotx.io/ai" -import { verifyAiProviderApiKey } from "@/features/integration-ai/lib/verify-api-key" +import { verifyAiProviderApiKey } from "@chatbotx.io/business/integration-ai-provider/verify" export const verifyDeepSeekApiKey = (apiKey: string) => - verifyAiProviderApiKey(aiProviders.enum.deepseek, apiKey) + verifyAiProviderApiKey("deepseek", apiKey) diff --git a/apps/builder/src/features/integration-drip/actions/disconnect.action.ts b/apps/builder/src/features/integration-drip/actions/disconnect.action.ts index 434c2c9943..1d2ddea2c4 100644 --- a/apps/builder/src/features/integration-drip/actions/disconnect.action.ts +++ b/apps/builder/src/features/integration-drip/actions/disconnect.action.ts @@ -5,5 +5,5 @@ import { createDisconnectAction } from "@/lib/integration-actions" export const disconnectDripAction = createDisconnectAction( integrationDripService, - { name: "Drip" }, + { name: "Drip", provider: "drip" }, ) diff --git a/apps/builder/src/features/integration-facebook-ads/actions/disconnect.action.ts b/apps/builder/src/features/integration-facebook-ads/actions/disconnect.action.ts index 543ba76a60..13d137c6e0 100644 --- a/apps/builder/src/features/integration-facebook-ads/actions/disconnect.action.ts +++ b/apps/builder/src/features/integration-facebook-ads/actions/disconnect.action.ts @@ -9,9 +9,9 @@ import { import { workspaceIdrequestParams } from "@/features/common/schema" import { assertWorkspaceSuperAdmin } from "@/lib/auth/assert-workspace-super-admin" import { logger } from "@/lib/log" -import { workspaceActionClient } from "@/lib/safe-action" +import { workspaceActionClientAllowExpired } from "@/lib/safe-action" -export const disconnectFacebookAdsAction = workspaceActionClient +export const disconnectFacebookAdsAction = workspaceActionClientAllowExpired .bindArgsSchemas(workspaceIdrequestParams) .action( async ({ diff --git a/apps/builder/src/features/integration-gemini/actions/connect.action.ts b/apps/builder/src/features/integration-gemini/actions/connect.action.ts index d8b336e093..54a281f790 100644 --- a/apps/builder/src/features/integration-gemini/actions/connect.action.ts +++ b/apps/builder/src/features/integration-gemini/actions/connect.action.ts @@ -2,13 +2,13 @@ import { aiProviders } from "@chatbotx.io/ai" import { aiIntegrationService } from "@chatbotx.io/ai/server" import { integrationGeminiService } from "@chatbotx.io/business" +import { verifyAiProviderApiKey } from "@chatbotx.io/business/integration-ai-provider/verify" import { getTranslations } from "next-intl/server" import { returnValidationErrors } from "next-safe-action" import { type WorkspaceIdRequestParams, workspaceIdrequestParams, } from "@/features/common/schema" -import { verifyAiProviderApiKey } from "@/features/integration-ai/lib/verify-api-key" import { workspaceActionClient } from "@/lib/safe-action" import { type ConnectGeminiRequest, @@ -28,12 +28,7 @@ export const connectGeminiAction = workspaceActionClient }) => { const t = await getTranslations() - if ( - !(await verifyAiProviderApiKey( - aiProviders.enum.gemini, - parsedInput.apiKey, - )) - ) { + if (!(await verifyAiProviderApiKey("gemini", parsedInput.apiKey))) { return returnValidationErrors(connectGeminiRequest, { apiKey: { _errors: [t("validation.invalidApiKey")], diff --git a/apps/builder/src/features/integration-gemini/actions/disconnect.action.ts b/apps/builder/src/features/integration-gemini/actions/disconnect.action.ts index 60135c6e39..d3a6c08e8f 100644 --- a/apps/builder/src/features/integration-gemini/actions/disconnect.action.ts +++ b/apps/builder/src/features/integration-gemini/actions/disconnect.action.ts @@ -9,6 +9,7 @@ export const disconnectGeminiAction = createDisconnectAction( integrationGeminiService, { name: "Gemini", + provider: "gemini", log: false, afterDisconnect: async (workspaceId) => { await aiIntegrationService.invalidateCache( diff --git a/apps/builder/src/features/integration-get-response/actions/disconnect.action.ts b/apps/builder/src/features/integration-get-response/actions/disconnect.action.ts index cd13be4f32..5a03b4c11a 100644 --- a/apps/builder/src/features/integration-get-response/actions/disconnect.action.ts +++ b/apps/builder/src/features/integration-get-response/actions/disconnect.action.ts @@ -5,5 +5,5 @@ import { createDisconnectAction } from "@/lib/integration-actions" export const disconnectGetResponseAction = createDisconnectAction( integrationGetResponseService, - { name: "GetResponse" }, + { name: "GetResponse", provider: "getResponse" }, ) diff --git a/apps/builder/src/features/integration-instagram/actions/connect-account-facebook.ts b/apps/builder/src/features/integration-instagram/actions/connect-account-facebook.ts index 2c798601a5..210e22d87b 100644 --- a/apps/builder/src/features/integration-instagram/actions/connect-account-facebook.ts +++ b/apps/builder/src/features/integration-instagram/actions/connect-account-facebook.ts @@ -4,181 +4,140 @@ import { buildContext, instagramIntegrationService, } from "@chatbotx.io/business" +import { connectionService } from "@chatbotx.io/connections" import type { InstagramAuthValue } from "@chatbotx.io/integration-instagram-facebook" +import { integration as integrationInstagramFacebook } from "@chatbotx.io/integration-instagram-facebook" import { - getUserInstagramAccounts, - integration as integrationInstagramFacebook, - subscribePageToInstagramWebhook, -} from "@chatbotx.io/integration-instagram-facebook" -import { AuthType } from "@chatbotx.io/sdk" + connectedOutcome, + duplicatedOutcome, + notSelectableOutcome, + runConnectFollowUps, + toConnectActionFailure, +} from "@/features/channel-connect/lib/connect-action-outcomes" import type { ResolvedConnectSession } from "@/features/channel-connect/lib/resolve-connect-session" -import { - type ConnectCandidateLookup, - runConnectSequence, - selectableCandidate, - unselectableCandidate, -} from "@/features/channel-connect/lib/run-connect-sequence" +import { resolveConnectSession } from "@/features/channel-connect/lib/resolve-connect-session" import type { ConnectActionResultWire } from "@/features/channel-connect/schema" import { BRANDING_TITLE } from "@/features/integration-webchat/lib" import { updateWorkspaceLogo } from "@/features/workspaces/actions/upload-logo" -import { FB_INSTAGRAM_FACEBOOK_PENDING_AUTH_COOKIE } from "@/lib/facebook-pending-auth" -import { persistIntegrationUserInfo } from "@/lib/integration-user-info" -import { isInstagramAccountConnected } from "./connect-account-shared" type InstagramFacebookSession = ResolvedConnectSession<"instagramFacebook"> -type LinkedInstagramAccount = { - id: string - name: string - username: string - pageId: string - pageAccessToken: string -} - /** - * One Graph call per request, no cache — provider lists carry page access - * tokens (plan §4.9). + * Connects one Instagram account (via its linked Facebook Page) from a + * `ConnectSession`, as a plain server function so both transports can call + * it: the oRPC route the picker posts to in parallel (`api/connect.ts`) and + * the server action kept for any non-picker caller. Same skeleton and the + * same two accepted scope reductions as `connectMessengerPage`. */ -async function findLinkedAccount({ - session, - sourceId, +export async function connectInstagramAccountViaFacebook({ + userId, + sessionId, + igId, }: { - session: InstagramFacebookSession - sourceId: string -}): Promise> { - const accounts = await getUserInstagramAccounts( - session.pendingAuth.userToken, - session.pendingAuth.version, - ) - const account = accounts.find((candidate) => candidate.id === sourceId) - - return account ? selectableCandidate(account) : unselectableCandidate() -} + userId: string + sessionId: string + igId: string +}): Promise { + let name = igId -async function subscribeAndPersistAccount({ - session, - candidate: account, - actorUserId, -}: { - session: InstagramFacebookSession - candidate: LinkedInstagramAccount - actorUserId: string -}) { - const { pendingAuth, workspace, platformOwnerId, brandingMenuEntry } = session - const instagramSettings = session.credential.config + try { + const session = await resolveConnectSession({ + userId, + sessionId, + credentialType: "instagramFacebook", + brandingChannel: "instagram", + }) - await subscribePageToInstagramWebhook({ - pageId: account.pageId, - accessToken: account.pageAccessToken, - version: pendingAuth.version, - }) + const target = session.session.targets.find((t) => t.id === igId) + if (!target) { + return notSelectableOutcome({ sourceId: igId, name }) + } + name = target.name - const auth: InstagramAuthValue = { - authType: AuthType.oauth2, - clientId: instagramSettings.clientId, - clientSecret: instagramSettings.clientSecret, - redirectUrl: "", - tokens: { - accessToken: account.pageAccessToken, - }, - metadata: { - igId: account.id, - igName: account.name, - pageId: account.pageId, - version: pendingAuth.version, - }, - } + if (!target.selectable) { + return target.alreadyConnected + ? duplicatedOutcome({ sourceId: igId, name }) + : notSelectableOutcome({ sourceId: igId, name }) + } - const { integrationId, integration } = - await instagramIntegrationService.connectAccount({ - actorUserId, - ownerId: platformOwnerId, - workspaceId: workspace.id, - type: "facebook", - account: { - igId: account.id, - igName: account.name, - igUsername: account.username, - pageId: account.pageId, - }, - auth, - persistentMenus: [brandingMenuEntry], + const result = await connectionService.connectTargets({ + sessionId, + workspaceId: session.workspace.id, + targetIds: [igId], + actorUserId: userId, }) + const outcome = result.outcomes[0] + const connection = result.connections[0] - return { - integrationId, - runFollowUps: async () => { - const brandingCtx = await buildContext({ - workspaceId: workspace.id, - integrationType: "instagramFacebook", - integration: { ...integration, auth }, - }) - - await integrationInstagramFacebook.runChannelHandler( - "bot", - "addBranding", - { - ctx: brandingCtx, - title: BRANDING_TITLE, - url: brandingMenuEntry.url, + if (!(outcome && outcome.status === "connected" && connection)) { + return { + kind: "outcome", + outcome: { + sourceId: igId, + name, + status: outcome?.status ?? "failed", + reason: outcome?.reason ?? "unknown", + detail: outcome?.detail, + coexistEligible: false, }, - ) + } + } + + const instagramRow = await instagramIntegrationService.findByInboxId( + connection.inboxId as string, + ) - await updateWorkspaceLogo({ - id: workspace.id, - integration: integrationInstagramFacebook, - ctx: brandingCtx, - }) + const warning = await runConnectFollowUps( + () => runInstagramFacebookFollowUps({ session, instagramRow }), + { + message: + "Instagram (via Facebook) connect follow-up failed after the account was connected", + }, + ) - await persistIntegrationUserInfo({ - workspaceId: workspace.id, - userId: pendingAuth.userId, - userName: pendingAuth.userName, - userAccessToken: pendingAuth.userToken, - avatarUrl: pendingAuth.userAvatarUrl, - persist: (userInfo) => - instagramIntegrationService.updateUserInfo({ - id: integrationId, - workspaceId: workspace.id, - userInfo, - }), - }) - }, + return connectedOutcome({ + sourceId: igId, + name, + warning, + integrationId: instagramRow.id, + coexistEligible: true, + }) + } catch (error) { + return toConnectActionFailure(error, { + sourceId: igId, + name, + log: "Failed to connect an Instagram account via Facebook", + }) } } -/** - * Connects a single Instagram account via its linked Facebook Page, as a - * plain server function so both transports can call it: the oRPC route the - * picker posts to in parallel (`api/connect.ts`) and the server action kept - * for any non-picker caller. Same skeleton as Messenger's - * `connectMessengerPage`: the pending-auth cookie is the only source of the - * user token / workspace, the id arrives on the wire alone, and every failure - * comes back as a typed `ConnectActionResult` instead of a thrown exception. - */ -export function connectInstagramAccountViaFacebook({ - userId, - igId, +async function runInstagramFacebookFollowUps({ + session, + instagramRow, }: { - userId: string - igId: string -}): Promise { - return runConnectSequence({ - sourceId: igId, - session: { - userId, - cookieName: FB_INSTAGRAM_FACEBOOK_PENDING_AUTH_COOKIE, - credentialType: "instagramFacebook", - brandingChannel: "instagram", - }, - lookUpCandidate: findLinkedAccount, - isAlreadyConnected: isInstagramAccountConnected, - connect: ({ session, candidate }) => - subscribeAndPersistAccount({ session, candidate, actorUserId: userId }), - logMessages: { - followUpFailed: - "Instagram (via Facebook) connect follow-up failed after the account was connected", - failed: "Failed to connect an Instagram account via Facebook", - }, + session: InstagramFacebookSession + instagramRow: Awaited< + ReturnType + > +}): Promise { + const { workspace, brandingMenuEntry } = session + const auth = instagramRow.auth as InstagramAuthValue + + const brandingCtx = await buildContext({ + workspaceId: workspace.id, + integrationType: "instagramFacebook", + integration: { ...instagramRow, auth }, + }) + + await integrationInstagramFacebook.runChannelHandler("bot", "addBranding", { + ctx: brandingCtx, + title: BRANDING_TITLE, + url: brandingMenuEntry.url, + }) + + await updateWorkspaceLogo({ + id: workspace.id, + integration: integrationInstagramFacebook, + ctx: brandingCtx, }) } diff --git a/apps/builder/src/features/integration-instagram/actions/connect-account.ts b/apps/builder/src/features/integration-instagram/actions/connect-account.ts index 56250d6dfe..b05f42d884 100644 --- a/apps/builder/src/features/integration-instagram/actions/connect-account.ts +++ b/apps/builder/src/features/integration-instagram/actions/connect-account.ts @@ -4,180 +4,142 @@ import { buildContext, instagramIntegrationService, } from "@chatbotx.io/business" +import { connectionService } from "@chatbotx.io/connections" import type { InstagramAuthValue } from "@chatbotx.io/integration-instagram" +import { integration as integrationInstagram } from "@chatbotx.io/integration-instagram" import { - getInstagramAccount, - integration as integrationInstagram, - subscribePageToInstagramWebhook, -} from "@chatbotx.io/integration-instagram" -import { AuthType } from "@chatbotx.io/sdk" + connectedOutcome, + duplicatedOutcome, + notSelectableOutcome, + runConnectFollowUps, + toConnectActionFailure, +} from "@/features/channel-connect/lib/connect-action-outcomes" import type { ResolvedConnectSession } from "@/features/channel-connect/lib/resolve-connect-session" -import { - type ConnectCandidateLookup, - runConnectSequence, - selectableCandidate, - unselectableCandidate, -} from "@/features/channel-connect/lib/run-connect-sequence" +import { resolveConnectSession } from "@/features/channel-connect/lib/resolve-connect-session" import type { ConnectActionResultWire } from "@/features/channel-connect/schema" import { BRANDING_TITLE } from "@/features/integration-webchat/lib" import { updateWorkspaceLogo } from "@/features/workspaces/actions/upload-logo" -import { FB_INSTAGRAM_PENDING_AUTH_COOKIE } from "@/lib/facebook-pending-auth" -import { persistIntegrationUserInfo } from "@/lib/integration-user-info" -import { isInstagramAccountConnected } from "./connect-account-shared" type InstagramSession = ResolvedConnectSession<"instagram"> -type BusinessLoginAccount = { - /** The Instagram Business Login page id. */ - id: string - /** The Instagram account id — the `igId` the picker sends on the wire. */ - userId: string - name: string - username: string - accessToken: string - profile_picture_url?: string -} - /** - * Re-resolves the account server-side. The picker only ever offers one - * Instagram Business Login account per session, so `sourceId` here is a - * cross-check against what the provider actually returns, not a lookup key. + * Connects the single Instagram Business Login account from a + * `ConnectSession`, as a plain server function so both transports can call + * it: the oRPC route the picker posts to (`api/connect.ts`) and the server + * action kept for any non-picker caller. Same skeleton and the same two + * accepted scope reductions as `connectMessengerPage` (no + * `persistIntegrationUserInfo`; `addBranding` is a live Graph push, not a DB + * write, so `IntegrationInstagram.persistentMenus` stays at its default). */ -async function findAuthorizedAccount({ - session, - sourceId, +export async function connectInstagramAccount({ + userId, + sessionId, + igId, }: { - session: InstagramSession - sourceId: string -}): Promise> { - const account = await getInstagramAccount(session.pendingAuth.userToken) - - return account && account.userId === sourceId - ? selectableCandidate(account) - : unselectableCandidate() -} + userId: string + sessionId: string + igId: string +}): Promise { + let name = igId -async function subscribeAndPersistAccount({ - session, - candidate: account, - actorUserId, -}: { - session: InstagramSession - candidate: BusinessLoginAccount - actorUserId: string -}) { - const { workspace, platformOwnerId, brandingMenuEntry } = session - const instagramSettings = session.credential.config + try { + const session = await resolveConnectSession({ + userId, + sessionId, + credentialType: "instagram", + brandingChannel: "instagram", + }) - await subscribePageToInstagramWebhook({ - igId: account.id, - accessToken: account.accessToken, - version: instagramSettings.version, - }) + const target = session.session.targets.find((t) => t.id === igId) + if (!target) { + return notSelectableOutcome({ sourceId: igId, name }) + } + name = target.name - const auth: InstagramAuthValue = { - authType: AuthType.oauth2, - clientId: instagramSettings.clientId, - clientSecret: instagramSettings.clientSecret, - redirectUrl: "", - tokens: { - accessToken: account.accessToken, - }, - metadata: { - igId: account.userId, - igName: account.name, - pageId: account.id, - version: instagramSettings.version, - }, - } + if (!target.selectable) { + return target.alreadyConnected + ? duplicatedOutcome({ sourceId: igId, name }) + : notSelectableOutcome({ sourceId: igId, name }) + } - const { integrationId, integration } = - await instagramIntegrationService.connectAccount({ - actorUserId, - ownerId: platformOwnerId, - workspaceId: workspace.id, - type: "instagram", - account: { - igId: account.userId, - igName: account.name, - igUsername: account.username, - pageId: account.id, - }, - auth, - persistentMenus: [brandingMenuEntry], + const result = await connectionService.connectTargets({ + sessionId, + workspaceId: session.workspace.id, + targetIds: [igId], + actorUserId: userId, }) + const outcome = result.outcomes[0] + const connection = result.connections[0] - return { - integrationId, - runFollowUps: async () => { - const brandingCtx = await buildContext({ - workspaceId: workspace.id, - integrationType: "instagram", - integration: { ...integration, auth }, - }) + if (!(outcome && outcome.status === "connected" && connection)) { + return { + kind: "outcome", + outcome: { + sourceId: igId, + name, + status: outcome?.status ?? "failed", + reason: outcome?.reason ?? "unknown", + detail: outcome?.detail, + coexistEligible: false, + }, + } + } - await integrationInstagram.runChannelHandler("bot", "addBranding", { - ctx: brandingCtx, - title: BRANDING_TITLE, - url: brandingMenuEntry.url, - }) + const instagramRow = await instagramIntegrationService.findByInboxId( + connection.inboxId as string, + ) - await updateWorkspaceLogo({ - id: workspace.id, - integration: integrationInstagram, - ctx: brandingCtx, - }) + const warning = await runConnectFollowUps( + () => runInstagramFollowUps({ session, instagramRow }), + { + message: + "Instagram connect follow-up failed after the account was connected", + }, + ) - // Direct Instagram login authenticates as the account itself, so the - // account IS the user — no extra Graph identity call needed. - await persistIntegrationUserInfo({ - workspaceId: workspace.id, - userId: account.userId, - userName: account.name, - userAccessToken: account.accessToken, - avatarUrl: account.profile_picture_url, - persist: (userInfo) => - instagramIntegrationService.updateUserInfo({ - id: integrationId, - workspaceId: workspace.id, - userInfo, - }), - }) - }, + return connectedOutcome({ + sourceId: igId, + name, + warning, + integrationId: instagramRow.id, + coexistEligible: true, + }) + } catch (error) { + return toConnectActionFailure(error, { + sourceId: igId, + name, + log: "Failed to connect an Instagram account", + }) } } -/** - * Connects the single Instagram Business Login account authorized by the - * pending OAuth session, as a plain server function so both transports can - * call it: the oRPC route the picker posts to (`api/connect.ts`) and the - * server action kept for any non-picker caller. Same session/item-outcome - * contract as `connectMessengerPage` and - * `connectInstagramAccountViaFacebook` (plan §2.4/§3.3). - */ -export function connectInstagramAccount({ - userId, - igId, +async function runInstagramFollowUps({ + session, + instagramRow, }: { - userId: string - igId: string -}): Promise { - return runConnectSequence({ - sourceId: igId, - session: { - userId, - cookieName: FB_INSTAGRAM_PENDING_AUTH_COOKIE, - credentialType: "instagram", - brandingChannel: "instagram", - }, - lookUpCandidate: findAuthorizedAccount, - isAlreadyConnected: isInstagramAccountConnected, - connect: ({ session, candidate }) => - subscribeAndPersistAccount({ session, candidate, actorUserId: userId }), - logMessages: { - followUpFailed: - "Instagram connect follow-up failed after the account was connected", - failed: "Failed to connect an Instagram account", - }, + session: InstagramSession + instagramRow: Awaited< + ReturnType + > +}): Promise { + const { workspace, brandingMenuEntry } = session + const auth = instagramRow.auth as InstagramAuthValue + + const brandingCtx = await buildContext({ + workspaceId: workspace.id, + integrationType: "instagram", + integration: { ...instagramRow, auth }, + }) + + await integrationInstagram.runChannelHandler("bot", "addBranding", { + ctx: brandingCtx, + title: BRANDING_TITLE, + url: brandingMenuEntry.url, + }) + + await updateWorkspaceLogo({ + id: workspace.id, + integration: integrationInstagram, + ctx: brandingCtx, }) } diff --git a/apps/builder/src/features/integration-instagram/api/connect.ts b/apps/builder/src/features/integration-instagram/api/connect.ts index 19552a05d8..f834965300 100644 --- a/apps/builder/src/features/integration-instagram/api/connect.ts +++ b/apps/builder/src/features/integration-instagram/api/connect.ts @@ -34,6 +34,7 @@ export const integrationInstagramConnectAPIs = { ({ input, context }): Promise => connectInstagramAccountViaFacebook({ userId: context.user.id, + sessionId: input.sessionId, igId: input.igId, }), ), @@ -53,6 +54,7 @@ export const integrationInstagramConnectAPIs = { ({ input, context }): Promise => connectInstagramAccount({ userId: context.user.id, + sessionId: input.sessionId, igId: input.igId, }), ), diff --git a/apps/builder/src/features/integration-instagram/components/instagram-accounts.tsx b/apps/builder/src/features/integration-instagram/components/instagram-accounts.tsx index d13d9e619c..e0f751fb8d 100644 --- a/apps/builder/src/features/integration-instagram/components/instagram-accounts.tsx +++ b/apps/builder/src/features/integration-instagram/components/instagram-accounts.tsx @@ -1,6 +1,5 @@ "use client" -import type { InstagramAccount } from "@chatbotx.io/integration-instagram" import { Button, buttonVariants } from "@chatbotx.io/ui/components/ui/button" import { Loader2Icon } from "lucide-react" import Image from "next/image" @@ -23,18 +22,32 @@ import { } from "@/features/channel-connect/lib/registry" import { connectActionResultSchemaDefault } from "@/features/channel-connect/schema" +/** The one account a direct-login `ConnectSession` offers — `session.targets[0]`, the public projection (no username/access token). */ +export type InstagramDirectAccount = { + id: string + name: string + avatarUrl?: string +} + /** - * Instagram direct-login connect (plan §3.3): a single account, so it always - * goes through `useConnectFlow`'s single-item path (inline spinner, then the + * Instagram direct-login connect: a single account, so it always goes + * through `useConnectFlow`'s single-item path (inline spinner, then the * coexist call when the account's own switch asked for one) — never the * multi-select batch dialog. + * + * Scope note: the account's `username` is no longer displayed — it lived + * only in the pending-auth cookie's live re-fetch, and the unified session + * model's public `session.targets` projection doesn't carry it (by design, + * the same projection every picker shares). A cosmetic-only regression. */ export function InstagramAccounts({ + sessionId, workspaceId, account, }: { + sessionId: string workspaceId: string - account: InstagramAccount + account: InstagramDirectAccount }) { const t = useTranslations() const router = useRouter() @@ -51,7 +64,7 @@ export function InstagramAccounts({ const connectOne = (item: ConnectTarget) => connectViaApi({ route: INSTAGRAM_DIRECT_CONNECT_ROUTE, - body: { igId: item.id }, + body: { sessionId, igId: item.id }, parse: (data) => connectActionResultSchemaDefault.parse(data), item, }) @@ -82,18 +95,17 @@ export function InstagramAccounts({ )}
- {account.profile_picture_url && ( + {account.avatarUrl && ( {account.name} )}

{account.name}

-

@{account.username}

{/* The same trailing column (and the same reveal rule) the multi-select pickers render, for the one account this screen @@ -127,7 +139,7 @@ export function InstagramAccounts({ onClick={() => flow.start([ { - id: account.userId, + id: account.id, name: account.name, aiReadsSyncedHistory, coexist: syncHistory, diff --git a/apps/builder/src/features/integration-instagram/components/select-accounts.tsx b/apps/builder/src/features/integration-instagram/components/select-accounts.tsx index e5c4a41d02..206c6e2c6e 100644 --- a/apps/builder/src/features/integration-instagram/components/select-accounts.tsx +++ b/apps/builder/src/features/integration-instagram/components/select-accounts.tsx @@ -1,6 +1,5 @@ "use client" -import type { InstagramAccount } from "@chatbotx.io/integration-instagram" import { Card, CardContent, @@ -9,14 +8,20 @@ import { } from "@chatbotx.io/ui/components/ui/card" import { useTranslations } from "next-intl" import { CONNECT_PICKER_CARD_CLASS } from "@/features/channel-connect/components/connect-picker-card" +import type { InstagramDirectAccount } from "@/features/integration-instagram/components/instagram-accounts" import { InstagramAccounts } from "@/features/integration-instagram/components/instagram-accounts" export type SelectAccountProps = { - account: InstagramAccount + sessionId: string + account: InstagramDirectAccount workspaceId: string } -export function SelectAccount({ account, workspaceId }: SelectAccountProps) { +export function SelectAccount({ + sessionId, + account, + workspaceId, +}: SelectAccountProps) { const t = useTranslations() return ( @@ -27,7 +32,11 @@ export function SelectAccount({ account, workspaceId }: SelectAccountProps) { - + ) diff --git a/apps/builder/src/features/integration-instagram/components/select-facebook-accounts.tsx b/apps/builder/src/features/integration-instagram/components/select-facebook-accounts.tsx index c22a036b22..7614a6ff9c 100644 --- a/apps/builder/src/features/integration-instagram/components/select-facebook-accounts.tsx +++ b/apps/builder/src/features/integration-instagram/components/select-facebook-accounts.tsx @@ -13,9 +13,11 @@ import { connectActionResultSchemaDefault } from "@/features/channel-connect/sch * "not an admin" state, only selectable vs. already-connected. */ export function SelectFacebookAccounts({ + sessionId, workspaceId, items, }: { + sessionId: string workspaceId: string items: ConnectPickerItem[] }) { @@ -23,7 +25,7 @@ export function SelectFacebookAccounts({ const connectOne = (item: ConnectPickerItem) => connectViaApi({ route: CONNECT_CHANNEL_REGISTRY.instagram.connectRoute, - body: { igId: item.id }, + body: { sessionId, igId: item.id }, parse: (data) => connectActionResultSchemaDefault.parse(data), item, }) diff --git a/apps/builder/src/features/integration-instagram/schema/action-facebook.ts b/apps/builder/src/features/integration-instagram/schema/action-facebook.ts index 55216ff997..007bfd3a56 100644 --- a/apps/builder/src/features/integration-instagram/schema/action-facebook.ts +++ b/apps/builder/src/features/integration-instagram/schema/action-facebook.ts @@ -1,12 +1,13 @@ import z from "zod" /** - * `workspaceId` and `version` never travel on the wire — the connect action - * re-derives both from the encrypted pending-auth cookie via - * `resolveConnectSession` (plan §2.4/§4.7), mirroring Messenger's - * `selectPageRequest`. The operator's pick is the Instagram account id - * alone; the action re-fetches the provider list itself. + * `workspaceId`/`version` never travel on the wire — the connect action + * re-derives both from the `ConnectSession` row via `resolveConnectSession`, + * mirroring Messenger's `selectPageRequest`. The operator's pick is the + * session id plus the Instagram account id; the account itself comes from + * `session.targets`, never a live re-fetch. */ export const selectFacebookAccountRequest = z.object({ + sessionId: z.string().min(1), igId: z.string().min(1), }) diff --git a/apps/builder/src/features/integration-instagram/schema/action.ts b/apps/builder/src/features/integration-instagram/schema/action.ts index 8248d8e4e6..54e810aa36 100644 --- a/apps/builder/src/features/integration-instagram/schema/action.ts +++ b/apps/builder/src/features/integration-instagram/schema/action.ts @@ -2,14 +2,13 @@ import { instagramPersistentMenuTypes } from "@chatbotx.io/database/partials" import z from "zod" /** - * `workspaceId` never travels on the wire — the connect action re-derives it - * from the encrypted pending-auth cookie via `resolveConnectSession` (plan - * §2.4/§4.7). The account itself is re-resolved server-side from - * `getInstagramAccount(cookie.userToken)` and cross-checked against `igId` - * (plan §3.3) — the wire payload carries nothing more than the id the - * operator picked. + * The account is re-resolved server-side from the `ConnectSession` row + * (`resolveConnectSession`) — the wire payload carries only the session id + * and the account id the operator picked, cross-checked against + * `session.targets`. */ export const selectAccountRequest = z.object({ + sessionId: z.string().min(1), igId: z.string().min(1), }) diff --git a/apps/builder/src/features/integration-klaviyo/actions/disconnect.action.ts b/apps/builder/src/features/integration-klaviyo/actions/disconnect.action.ts index 4ac371c3e9..ec56b2dac7 100644 --- a/apps/builder/src/features/integration-klaviyo/actions/disconnect.action.ts +++ b/apps/builder/src/features/integration-klaviyo/actions/disconnect.action.ts @@ -5,5 +5,5 @@ import { createDisconnectAction } from "@/lib/integration-actions" export const disconnectKlaviyoAction = createDisconnectAction( integrationKlaviyoService, - { name: "Klaviyo" }, + { name: "Klaviyo", provider: "klaviyo" }, ) diff --git a/apps/builder/src/features/integration-mailchimp/actions/disconnect.action.ts b/apps/builder/src/features/integration-mailchimp/actions/disconnect.action.ts index 88fa3b81e7..96ef6a9346 100644 --- a/apps/builder/src/features/integration-mailchimp/actions/disconnect.action.ts +++ b/apps/builder/src/features/integration-mailchimp/actions/disconnect.action.ts @@ -5,5 +5,5 @@ import { createDisconnectAction } from "@/lib/integration-actions" export const disconnectMailchimpAction = createDisconnectAction( integrationMailchimpService, - { name: "Mailchimp" }, + { name: "Mailchimp", provider: "mailchimp" }, ) diff --git a/apps/builder/src/features/integration-mailer-lite/actions/disconnect.action.ts b/apps/builder/src/features/integration-mailer-lite/actions/disconnect.action.ts index 0ff970ae48..d9d15220dc 100644 --- a/apps/builder/src/features/integration-mailer-lite/actions/disconnect.action.ts +++ b/apps/builder/src/features/integration-mailer-lite/actions/disconnect.action.ts @@ -5,5 +5,5 @@ import { createDisconnectAction } from "@/lib/integration-actions" export const disconnectMailerLiteAction = createDisconnectAction( integrationMailerLiteService, - { name: "MailerLite" }, + { name: "MailerLite", provider: "mailerLite" }, ) diff --git a/apps/builder/src/features/integration-messenger/actions/connect-page.ts b/apps/builder/src/features/integration-messenger/actions/connect-page.ts index c1d61a6d14..d8b864f8ca 100644 --- a/apps/builder/src/features/integration-messenger/actions/connect-page.ts +++ b/apps/builder/src/features/integration-messenger/actions/connect-page.ts @@ -5,198 +5,173 @@ import { messengerIntegrationService, tagSyncService, } from "@chatbotx.io/business" +import { connectionService } from "@chatbotx.io/connections" import { channelTypes } from "@chatbotx.io/database/partials" import type { MessengerAuthValue } from "@chatbotx.io/integration-messenger" +import { integration as integrationMessenger } from "@chatbotx.io/integration-messenger" import { - getUserPages, - integration as integrationMessenger, -} from "@chatbotx.io/integration-messenger" -import { - exchangeLongLivedToken, - subscribePageToAppWebhook, -} from "@chatbotx.io/integration-messenger/apis/page" -import { AuthType } from "@chatbotx.io/sdk" + connectedOutcome, + duplicatedOutcome, + notSelectableOutcome, + runConnectFollowUps, + toConnectActionFailure, +} from "@/features/channel-connect/lib/connect-action-outcomes" import type { ResolvedConnectSession } from "@/features/channel-connect/lib/resolve-connect-session" -import { - type ConnectCandidateLookup, - runConnectSequence, - selectableCandidate, - unselectableCandidate, -} from "@/features/channel-connect/lib/run-connect-sequence" +import { resolveConnectSession } from "@/features/channel-connect/lib/resolve-connect-session" import type { ConnectActionResultWire } from "@/features/channel-connect/schema" import { BRANDING_TITLE } from "@/features/integration-webchat/lib" import { updateWorkspaceLogo } from "@/features/workspaces/actions/upload-logo" -import { FB_MESSENGER_PENDING_AUTH_COOKIE } from "@/lib/facebook-pending-auth" -import { persistIntegrationUserInfo } from "@/lib/integration-user-info" type MessengerSession = ResolvedConnectSession<"messenger"> -type MessengerPageCandidate = { - pageId: string - name: string - pageAccessToken: string -} - /** - * One Graph call per request, no cache — provider lists carry page access - * tokens (plan §4.9). + * Connects one Facebook page from a `ConnectSession` in `awaiting_selection`, + * as a plain server function so both transports can call it: the oRPC route + * the batch picker posts to (`api/connect.ts`, `CONNECT_CONCURRENCY` at a + * time) and the server action kept for any non-picker caller. + * + * Replaces the pending-auth-cookie skeleton (`runConnectSequence` + + * `getUserPages` re-fetch + `messengerIntegrationService.connectPage`) with + * the unified `ConnectionService.connectTargets`, which already does the + * lookup/duplicate/quota/FSM/webhook-subscribe work generically. This + * function's own job shrinks to: resolve session context (workspace, + * branding), call `connectTargets` for the one target, then run Messenger's + * own post-connect follow-ups (persistent-menu branding, workspace-logo + * push, tag-sync enqueue) that `connectTargets` deliberately does not — + * those are product features layered on top of the generic connect, not + * part of it. + * + * Two deliberate, minor, display-only scope reductions versus the old flow + * (both accepted rather than adding more plumbing to a generic connect + * path for a legacy-only need): + * - No `persistIntegrationUserInfo` call: that recorded the connecting + * Facebook user's own identity (name/avatar) from the *user-level* OAuth + * token. The unified session model only retains each candidate's own + *page-level* token past `listCandidates`, so that identity isn't + * available here. + * - `addBranding` pushes the persistent-menu entry straight to the Graph + * API (it reads/writes the page's *live* menu, never the DB), so the + * branding link still reaches end users — but `IntegrationMessenger + * .persistentMenus` (the DB's own local record, used by the persistent- + * menu settings UI) is left at its default `[]` instead of pre-seeded + * with this entry, unlike the old `connectPage({persistentMenus: + * [brandingMenuEntry]})` insert-time value. */ -async function findConnectablePage({ - session, - sourceId, +export async function connectMessengerPage({ + userId, + sessionId, + pageId, }: { - session: MessengerSession - sourceId: string -}): Promise> { - const { pages } = await getUserPages( - session.pendingAuth.userToken, - session.pendingAuth.version, - ) - const page = pages.find((candidate) => candidate.id === sourceId) - - if (!(page?.isConnectable && page.access_token)) { - return unselectableCandidate(page?.name) - } + userId: string + sessionId: string + pageId: string +}): Promise { + let name = pageId - return selectableCandidate({ - pageId: page.id, - name: page.name, - pageAccessToken: page.access_token, - }) -} + try { + const session = await resolveConnectSession({ + userId, + sessionId, + credentialType: "messenger", + brandingChannel: "messenger", + }) -async function isPageConnected(pageId: string): Promise { - const connectedPageIds = - await messengerIntegrationService.findConnectedPageIds([pageId]) - return connectedPageIds.has(pageId) + const target = session.session.targets.find((t) => t.id === pageId) + if (!target) { + return notSelectableOutcome({ sourceId: pageId, name }) + } + name = target.name + + if (!target.selectable) { + return target.alreadyConnected + ? duplicatedOutcome({ sourceId: pageId, name }) + : notSelectableOutcome({ sourceId: pageId, name }) + } + + const result = await connectionService.connectTargets({ + sessionId, + workspaceId: session.workspace.id, + targetIds: [pageId], + actorUserId: userId, + }) + const outcome = result.outcomes[0] + const connection = result.connections[0] + + if (!(outcome && outcome.status === "connected" && connection)) { + return { + kind: "outcome", + outcome: { + sourceId: pageId, + name, + status: outcome?.status ?? "failed", + reason: outcome?.reason ?? "unknown", + detail: outcome?.detail, + coexistEligible: false, + }, + } + } + + const messengerRow = await messengerIntegrationService.findByInboxId( + connection.inboxId as string, + ) + + const warning = await runConnectFollowUps( + () => runMessengerFollowUps({ session, messengerRow }), + { + message: + "Messenger connect follow-up failed after the page was connected", + }, + ) + + return connectedOutcome({ + sourceId: pageId, + name, + warning, + integrationId: messengerRow.id, + coexistEligible: true, + }) + } catch (error) { + return toConnectActionFailure(error, { + sourceId: pageId, + name, + log: "Failed to connect a Messenger page", + }) + } } -async function subscribeAndPersistPage({ +async function runMessengerFollowUps({ session, - candidate, - actorUserId, + messengerRow, }: { session: MessengerSession - candidate: MessengerPageCandidate - actorUserId: string -}) { - const { pendingAuth, workspace, platformOwnerId, brandingMenuEntry } = session - const messengerSettings = session.credential.config - const { pageId, name: pageName } = candidate - - const longLivedToken = await exchangeLongLivedToken( - messengerSettings, - candidate.pageAccessToken, - ) - await subscribePageToAppWebhook({ - pageId, - accessToken: longLivedToken, - version: messengerSettings.version, + messengerRow: Awaited< + ReturnType + > +}): Promise { + const { workspace, brandingMenuEntry } = session + const auth = messengerRow.auth as MessengerAuthValue + + const brandingCtx = await buildContext({ + workspaceId: workspace.id, + integrationType: "messenger", + integration: { ...messengerRow, auth }, }) - const auth: MessengerAuthValue = { - authType: AuthType.oauth2, - clientId: messengerSettings.clientId, - clientSecret: messengerSettings.clientSecret, - redirectUrl: "", - version: messengerSettings.version, - tokens: { - accessToken: longLivedToken, - }, - metadata: { - pageId, - pageName, - version: messengerSettings.version, - }, - } - - const { integrationId, integration } = - await messengerIntegrationService.connectPage({ - actorUserId, - ownerId: platformOwnerId, - workspaceId: workspace.id, - page: { pageId, pageName }, - auth, - persistentMenus: [brandingMenuEntry], - }) + await integrationMessenger.runChannelHandler("bot", "addBranding", { + ctx: brandingCtx, + title: BRANDING_TITLE, + url: brandingMenuEntry.url, + }) - return { - integrationId, - runFollowUps: async () => { - const brandingCtx = await buildContext({ - workspaceId: workspace.id, - integrationType: "messenger", - integration: { ...integration, auth }, - }) - - await integrationMessenger.runChannelHandler("bot", "addBranding", { - ctx: brandingCtx, - title: BRANDING_TITLE, - url: brandingMenuEntry.url, - }) - - await updateWorkspaceLogo({ - id: workspace.id, - integration: integrationMessenger, - ctx: brandingCtx, - }) - - await persistIntegrationUserInfo({ - workspaceId: workspace.id, - userId: pendingAuth.userId, - userName: pendingAuth.userName, - userAccessToken: pendingAuth.userToken, - avatarUrl: pendingAuth.userAvatarUrl, - persist: (userInfo) => - messengerIntegrationService.updateUserInfo({ - id: integrationId, - workspaceId: workspace.id, - userInfo, - }), - }) - - await tagSyncService.enqueueChannelScan({ - workspaceId: workspace.id, - channelType: channelTypes.enum.messenger, - integrationId, - }) - }, - } -} + await updateWorkspaceLogo({ + id: workspace.id, + integration: integrationMessenger, + ctx: brandingCtx, + }) -/** - * Connects a single Facebook page, as a plain server function so both - * transports can call it: the oRPC route the picker posts to in parallel - * (`api/connect.ts`) and the server action kept for any non-picker caller. - * The pending-auth cookie is the only source of the user token / workspace, - * the id arrives on the wire alone, and every failure — session-level or - * item-level — comes back as a typed `ConnectActionResult` instead of a - * thrown exception. The skeleton around the Messenger-specific steps is - * shared with both Instagram cores via `runConnectSequence`. - */ -export function connectMessengerPage({ - userId, - pageId, -}: { - userId: string - pageId: string -}): Promise { - return runConnectSequence({ - sourceId: pageId, - session: { - userId, - cookieName: FB_MESSENGER_PENDING_AUTH_COOKIE, - credentialType: "messenger", - brandingChannel: "messenger", - }, - lookUpCandidate: findConnectablePage, - isAlreadyConnected: isPageConnected, - connect: ({ session, candidate }) => - subscribeAndPersistPage({ session, candidate, actorUserId: userId }), - logMessages: { - followUpFailed: - "Messenger connect follow-up failed after the page was connected", - failed: "Failed to connect a Messenger page", - }, + await tagSyncService.enqueueChannelScan({ + workspaceId: workspace.id, + channelType: channelTypes.enum.messenger, + integrationId: messengerRow.id, }) } diff --git a/apps/builder/src/features/integration-messenger/api/connect.ts b/apps/builder/src/features/integration-messenger/api/connect.ts index c228e280ba..575064cd07 100644 --- a/apps/builder/src/features/integration-messenger/api/connect.ts +++ b/apps/builder/src/features/integration-messenger/api/connect.ts @@ -35,6 +35,7 @@ export const integrationMessengerConnectAPIs = { ({ input, context }): Promise => connectMessengerPage({ userId: context.user.id, + sessionId: input.sessionId, pageId: input.pageId, }), ), diff --git a/apps/builder/src/features/integration-messenger/api/public.ts b/apps/builder/src/features/integration-messenger/api/public.ts index 6236897d1e..d46010ea8c 100644 --- a/apps/builder/src/features/integration-messenger/api/public.ts +++ b/apps/builder/src/features/integration-messenger/api/public.ts @@ -4,7 +4,7 @@ import { z } from "zod" import { possibleErrorsOnMutatingResource } from "@/lib/orpc/orpc-error-helper" import { workspaceTokenAuthAPIForScope } from "@/orpc" -const workspaceTokenAuthAPI = workspaceTokenAuthAPIForScope("channels") +const workspaceTokenAuthAPI = workspaceTokenAuthAPIForScope("connections") export const messengerChannelsPublicRouter = { updateTagSync: workspaceTokenAuthAPI diff --git a/apps/builder/src/features/integration-messenger/components/messenger-pages.tsx b/apps/builder/src/features/integration-messenger/components/messenger-pages.tsx index 4a0a75c02d..a1760f1884 100644 --- a/apps/builder/src/features/integration-messenger/components/messenger-pages.tsx +++ b/apps/builder/src/features/integration-messenger/components/messenger-pages.tsx @@ -29,9 +29,11 @@ export type MessengerPickerItem = ConnectPickerItem & { } export function MessengerPages({ + sessionId, workspaceId, items, }: { + sessionId: string workspaceId: string items: MessengerPickerItem[] }) { @@ -43,7 +45,7 @@ export function MessengerPages({ const connectOne = (item: MessengerPickerItem) => connectViaApi({ route: CONNECT_CHANNEL_REGISTRY.messenger.connectRoute, - body: { pageId: item.id }, + body: { sessionId, pageId: item.id }, parse: (data) => connectActionResultSchemaDefault.parse(data), item, }) diff --git a/apps/builder/src/features/integration-messenger/components/select-account.tsx b/apps/builder/src/features/integration-messenger/components/select-account.tsx index 17b011703e..4b8a27fc33 100644 --- a/apps/builder/src/features/integration-messenger/components/select-account.tsx +++ b/apps/builder/src/features/integration-messenger/components/select-account.tsx @@ -17,12 +17,14 @@ import type { MessengerPickerItem } from "@/features/integration-messenger/compo import { MessengerPages } from "@/features/integration-messenger/components/messenger-pages" type SelectPageProps = { + sessionId: string items: MessengerPickerItem[] bmLookupFailed: boolean workspaceId: string } export function SelectPage({ + sessionId, items, bmLookupFailed, workspaceId, @@ -47,7 +49,11 @@ export function SelectPage({ )} - + ) diff --git a/apps/builder/src/features/integration-messenger/message-templates/actions/sync-message-templates.ts b/apps/builder/src/features/integration-messenger/message-templates/actions/sync-message-templates.ts index 74fc6ce96b..2115866368 100644 --- a/apps/builder/src/features/integration-messenger/message-templates/actions/sync-message-templates.ts +++ b/apps/builder/src/features/integration-messenger/message-templates/actions/sync-message-templates.ts @@ -6,6 +6,7 @@ import { messengerMessageTemplateService, } from "@chatbotx.io/business" import type { IntegrationMessengerModel } from "@chatbotx.io/database/types" +import type { MessengerMessageTemplateEntity } from "@chatbotx.io/integration-messenger/apis/message-templates" import type { MessengerAuthValue } from "@chatbotx.io/integration-messenger/schema" import { invalidateCacheByTags } from "@chatbotx.io/redis" import { SdkException } from "@chatbotx.io/sdk" @@ -49,21 +50,23 @@ export async function syncMessengerMessageTemplatesForIntegration({ ) } - const templates = res.data.filter((template) => { - if (templateId && template.id !== templateId) { - return false - } + const templates = res.data.filter( + (template: MessengerMessageTemplateEntity) => { + if (templateId && template.id !== templateId) { + return false + } - if (templateName && template.name !== templateName) { - return false - } + if (templateName && template.name !== templateName) { + return false + } - if (templateLanguage && template.language !== templateLanguage) { - return false - } + if (templateLanguage && template.language !== templateLanguage) { + return false + } - return true - }) + return true + }, + ) await messengerMessageTemplateService.syncFromMeta({ integrationMessengerId: integrationMessenger.id, diff --git a/apps/builder/src/features/integration-messenger/schema/action.ts b/apps/builder/src/features/integration-messenger/schema/action.ts index bd796ab9d3..3e1308aae5 100644 --- a/apps/builder/src/features/integration-messenger/schema/action.ts +++ b/apps/builder/src/features/integration-messenger/schema/action.ts @@ -7,6 +7,7 @@ import { zodBigintAsString } from "@chatbotx.io/utils" import z from "zod" export const selectPageRequest = z.object({ + sessionId: z.string().min(1), pageId: z.string().min(1), }) diff --git a/apps/builder/src/features/integration-moosend/actions/disconnect.action.ts b/apps/builder/src/features/integration-moosend/actions/disconnect.action.ts index 21112192e3..ce80484f12 100644 --- a/apps/builder/src/features/integration-moosend/actions/disconnect.action.ts +++ b/apps/builder/src/features/integration-moosend/actions/disconnect.action.ts @@ -5,5 +5,5 @@ import { createDisconnectAction } from "@/lib/integration-actions" export const disconnectMoosendAction = createDisconnectAction( integrationMoosendService, - { name: "Moosend" }, + { name: "Moosend", provider: "moosend" }, ) diff --git a/apps/builder/src/features/integration-openai-compatible/actions/disconnect.action.ts b/apps/builder/src/features/integration-openai-compatible/actions/disconnect.action.ts index 131674bcca..2bccd1485d 100644 --- a/apps/builder/src/features/integration-openai-compatible/actions/disconnect.action.ts +++ b/apps/builder/src/features/integration-openai-compatible/actions/disconnect.action.ts @@ -5,19 +5,20 @@ import { type WorkspaceIdAndIdRequestParams, workspaceIdAndIdRequestParams, } from "@/features/common/schema" -import { workspaceActionClient } from "@/lib/safe-action" +import { workspaceActionClientAllowExpired } from "@/lib/safe-action" -export const disconnectOpenaiCompatibleAction = workspaceActionClient - .bindArgsSchemas(workspaceIdAndIdRequestParams) - .action( - async ({ - bindArgsParsedInputs: [workspaceId, integrationId], - }: { - bindArgsParsedInputs: WorkspaceIdAndIdRequestParams - }) => { - await integrationOpenaiCompatibleService.disconnect( - workspaceId, - integrationId, - ) - }, - ) +export const disconnectOpenaiCompatibleAction = + workspaceActionClientAllowExpired + .bindArgsSchemas(workspaceIdAndIdRequestParams) + .action( + async ({ + bindArgsParsedInputs: [workspaceId, integrationId], + }: { + bindArgsParsedInputs: WorkspaceIdAndIdRequestParams + }) => { + await integrationOpenaiCompatibleService.disconnect( + workspaceId, + integrationId, + ) + }, + ) diff --git a/apps/builder/src/features/integration-openai/actions/connect.action.ts b/apps/builder/src/features/integration-openai/actions/connect.action.ts index b226942876..b7bf6cdb07 100644 --- a/apps/builder/src/features/integration-openai/actions/connect.action.ts +++ b/apps/builder/src/features/integration-openai/actions/connect.action.ts @@ -3,13 +3,13 @@ import { aiProviders } from "@chatbotx.io/ai" import { aiIntegrationService } from "@chatbotx.io/ai/server" import { integrationOpenAIService } from "@chatbotx.io/business" +import { verifyAiProviderApiKey } from "@chatbotx.io/business/integration-ai-provider/verify" import { getTranslations } from "next-intl/server" import { returnValidationErrors } from "next-safe-action" import { type WorkspaceIdRequestParams, workspaceIdrequestParams, } from "@/features/common/schema" -import { verifyAiProviderApiKey } from "@/features/integration-ai/lib/verify-api-key" import { workspaceActionClient } from "@/lib/safe-action" import { type ConnectOpenAISchema, @@ -29,12 +29,7 @@ export const connectOpenAIAction = workspaceActionClient }) => { const t = await getTranslations() - if ( - !(await verifyAiProviderApiKey( - aiProviders.enum.openai, - parsedInput.apiKey, - )) - ) { + if (!(await verifyAiProviderApiKey("openai", parsedInput.apiKey))) { return returnValidationErrors(connectOpenAISchema, { apiKey: { _errors: [t("validation.invalidApiKey")], diff --git a/apps/builder/src/features/integration-openai/actions/disconnect.action.ts b/apps/builder/src/features/integration-openai/actions/disconnect.action.ts index 68e222be79..4457a5e01d 100644 --- a/apps/builder/src/features/integration-openai/actions/disconnect.action.ts +++ b/apps/builder/src/features/integration-openai/actions/disconnect.action.ts @@ -9,6 +9,7 @@ export const disconnectOpenAIAction = createDisconnectAction( integrationOpenAIService, { name: "OpenAI", + provider: "openai", log: false, afterDisconnect: async (workspaceId) => { await aiIntegrationService.invalidateCache( diff --git a/apps/builder/src/features/integration-openrouter/actions/disconnect.action.ts b/apps/builder/src/features/integration-openrouter/actions/disconnect.action.ts index 22b57f6ad1..98889b45e9 100644 --- a/apps/builder/src/features/integration-openrouter/actions/disconnect.action.ts +++ b/apps/builder/src/features/integration-openrouter/actions/disconnect.action.ts @@ -9,6 +9,7 @@ export const disconnectOpenRouterAction = createDisconnectAction( integrationOpenRouterService, { name: "OpenRouter", + provider: "openrouter", log: false, afterDisconnect: async (workspaceId) => { await aiIntegrationService.invalidateCache( diff --git a/apps/builder/src/features/integration-openrouter/lib/index.ts b/apps/builder/src/features/integration-openrouter/lib/index.ts index 3c116d7fcc..0c9737d623 100644 --- a/apps/builder/src/features/integration-openrouter/lib/index.ts +++ b/apps/builder/src/features/integration-openrouter/lib/index.ts @@ -1,5 +1,4 @@ -import { aiProviders } from "@chatbotx.io/ai" -import { verifyAiProviderApiKey } from "@/features/integration-ai/lib/verify-api-key" +import { verifyAiProviderApiKey } from "@chatbotx.io/business/integration-ai-provider/verify" export const verifyOpenRouterApiKey = (apiKey: string) => - verifyAiProviderApiKey(aiProviders.enum.openrouter, apiKey) + verifyAiProviderApiKey("openrouter", apiKey) diff --git a/apps/builder/src/features/integration-sendgrid/actions/disconnect.action.ts b/apps/builder/src/features/integration-sendgrid/actions/disconnect.action.ts index 99c4250248..5bd0f5dd81 100644 --- a/apps/builder/src/features/integration-sendgrid/actions/disconnect.action.ts +++ b/apps/builder/src/features/integration-sendgrid/actions/disconnect.action.ts @@ -5,5 +5,5 @@ import { createDisconnectAction } from "@/lib/integration-actions" export const disconnectSendGridAction = createDisconnectAction( integrationSendGridService, - { name: "SendGrid" }, + { name: "SendGrid", provider: "sendGrid" }, ) diff --git a/apps/builder/src/features/integration-smtp/actions/delete-smtp.action.ts b/apps/builder/src/features/integration-smtp/actions/delete-smtp.action.ts index 63d23c6ee4..7c269ecafe 100644 --- a/apps/builder/src/features/integration-smtp/actions/delete-smtp.action.ts +++ b/apps/builder/src/features/integration-smtp/actions/delete-smtp.action.ts @@ -2,9 +2,9 @@ import { integrationSmtpService, workspaceService } from "@chatbotx.io/business" import { zodBigintAsString } from "@chatbotx.io/utils" -import { workspaceActionClient } from "@/lib/safe-action" +import { workspaceActionClientAllowExpired } from "@/lib/safe-action" -export const deleteSmtpAction = workspaceActionClient +export const deleteSmtpAction = workspaceActionClientAllowExpired .bindArgsSchemas([zodBigintAsString(), zodBigintAsString()]) .action(async (props) => { const { diff --git a/apps/builder/src/features/integration-smtp/api/public.ts b/apps/builder/src/features/integration-smtp/api/public.ts index 056363599f..cd1d1d21bc 100644 --- a/apps/builder/src/features/integration-smtp/api/public.ts +++ b/apps/builder/src/features/integration-smtp/api/public.ts @@ -21,7 +21,7 @@ import { integrationSmtpResource, } from "../schema/resource" -const workspaceTokenAuthAPI = workspaceTokenAuthAPIForScope("channels") +const workspaceTokenAuthAPI = workspaceTokenAuthAPIForScope("connections") const tags = ["Channels"] diff --git a/apps/builder/src/features/integration-webchat/actions/delete-webchat.action.ts b/apps/builder/src/features/integration-webchat/actions/delete-webchat.action.ts index 35517584d5..6be29a1208 100644 --- a/apps/builder/src/features/integration-webchat/actions/delete-webchat.action.ts +++ b/apps/builder/src/features/integration-webchat/actions/delete-webchat.action.ts @@ -5,9 +5,9 @@ import { type WorkspaceIdAndIdRequestParams, workspaceIdAndIdRequestParams, } from "@/features/common/schema" -import { workspaceActionClient } from "@/lib/safe-action" +import { workspaceActionClientAllowExpired } from "@/lib/safe-action" -export const deleteWebchatAction = workspaceActionClient +export const deleteWebchatAction = workspaceActionClientAllowExpired .bindArgsSchemas(workspaceIdAndIdRequestParams) .action( async ({ diff --git a/apps/builder/src/features/integration-webchat/api/public.ts b/apps/builder/src/features/integration-webchat/api/public.ts index f6d3d444e9..11df33e853 100644 --- a/apps/builder/src/features/integration-webchat/api/public.ts +++ b/apps/builder/src/features/integration-webchat/api/public.ts @@ -20,7 +20,7 @@ import { webchatPublicResource, } from "../schema/public" -const workspaceTokenAuthAPI = workspaceTokenAuthAPIForScope("channels") +const workspaceTokenAuthAPI = workspaceTokenAuthAPIForScope("connections") const tags = ["Channels"] diff --git a/apps/builder/src/features/integration-webchat/lib/widget-css.tsx b/apps/builder/src/features/integration-webchat/lib/widget-css.tsx index b32b83e2c3..c82ea956b2 100644 --- a/apps/builder/src/features/integration-webchat/lib/widget-css.tsx +++ b/apps/builder/src/features/integration-webchat/lib/widget-css.tsx @@ -22,7 +22,7 @@ export const sanitizeWidgetCss = (css: string): string => * * Security: `customCss` is gated by `requireWorkspacePermission(workspaceId, * "superAdmin")` on the edit page and by the matching check inside - * `updateWebchatAction`. The `channels`-scoped public API + * `updateWebchatAction`. The `connections`-scoped public API * (`integration-webchat/api/public.ts`) accepts `customCss` too, with no * equivalent in-handler permission check — that is intentional, not a gap: * minting any workspace token already requires the caller to be a workspace diff --git a/apps/builder/src/features/integration-zalo/api/public.ts b/apps/builder/src/features/integration-zalo/api/public.ts index eec52203fb..d471534bf0 100644 --- a/apps/builder/src/features/integration-zalo/api/public.ts +++ b/apps/builder/src/features/integration-zalo/api/public.ts @@ -4,7 +4,7 @@ import { z } from "zod" import { possibleErrorsOnMutatingResource } from "@/lib/orpc/orpc-error-helper" import { workspaceTokenAuthAPIForScope } from "@/orpc" -const workspaceTokenAuthAPI = workspaceTokenAuthAPIForScope("channels") +const workspaceTokenAuthAPI = workspaceTokenAuthAPIForScope("connections") export const zaloChannelsPublicRouter = { updateTagSync: workspaceTokenAuthAPI diff --git a/apps/builder/src/features/integrations/api/public/ai.ts b/apps/builder/src/features/integrations/api/public/ai.ts index 9b630b4433..2ee9cce929 100644 --- a/apps/builder/src/features/integrations/api/public/ai.ts +++ b/apps/builder/src/features/integrations/api/public/ai.ts @@ -6,11 +6,9 @@ import { integrationGeminiService, integrationOpenAIService, } from "@chatbotx.io/business" -import { - notFoundException, - validationException, -} from "@chatbotx.io/business/errors" -import { verifyAiProviderApiKey } from "@/features/integration-ai/lib/verify-api-key" +import { notFoundException } from "@chatbotx.io/business/errors" +import { connectionService } from "@chatbotx.io/connections" +import { connectionRepository } from "@chatbotx.io/database/repositories" import { possibleErrorsOnFindingResource, possibleErrorsOnMutatingResource, @@ -23,7 +21,7 @@ import { publicAiProviderResource, } from "../../schema/ai-provider" -const workspaceTokenAuthAPI = workspaceTokenAuthAPIForScope("integrations") +const workspaceTokenAuthAPI = workspaceTokenAuthAPIForScope("connections") type AiProviderRow = { id: string @@ -54,14 +52,6 @@ const aiProviderServices = { findByWorkspaceId: ( workspaceId: string, ) => Promise - connect: (input: { - workspaceId: string - apiKey: string - model: string - temperature: number - maxOutputTokens: number - }) => Promise - disconnect: (workspaceId: string) => Promise } > @@ -91,7 +81,8 @@ export const integrationsAiPublicRouter = { path: "/v1/integrations/ai/{provider}", summary: "Connect or update an AI provider integration", description: - "Upserts the AI provider integration for the workspace — connects it if not already configured, otherwise replaces the stored configuration (including the API key).", + "Deprecated — use `POST /v1/connections` instead. Upserts the AI provider integration for the workspace — connects it if not already configured, otherwise replaces the stored configuration (including the API key).", + deprecated: true, tags: ["Integrations"], }) .input(getAiProviderRequest.extend(connectAiProviderRequest.shape)) @@ -100,16 +91,20 @@ export const integrationsAiPublicRouter = { .handler(async ({ context, input }) => { const service = aiProviderServices[input.provider] - if (!(await verifyAiProviderApiKey(input.provider, input.apiKey))) { - throw validationException("apiKey", "Invalid API key") - } - - await service.connect({ + // `allowUpdate: true` — this route has always upserted (replacing an + // already-connected provider's stored API key/config on repeat + // calls), unlike the strict "reject if already connected" semantics + // `connectFromCredentials` otherwise enforces for a fresh connect. + await connectionService.connectFromCredentials({ workspaceId: context.workspace.id, - apiKey: input.apiKey, - model: input.model, - temperature: input.temperature, - maxOutputTokens: input.maxOutputTokens, + provider: input.provider, + config: { + apiKey: input.apiKey, + model: input.model, + temperature: input.temperature, + maxOutputTokens: input.maxOutputTokens, + }, + allowUpdate: true, }) await aiIntegrationService.invalidateCache( @@ -129,14 +124,29 @@ export const integrationsAiPublicRouter = { method: "DELETE", path: "/v1/integrations/ai/{provider}", summary: "Disconnect an AI provider integration", + description: + "Deprecated — use `DELETE /v1/connections/{id}` instead. Kept for backward compatibility.", + deprecated: true, tags: ["Integrations"], successStatus: 204, }) .input(getAiProviderRequest) .errors(possibleErrorsOnMutatingResource) .handler(async ({ context, input }) => { - const service = aiProviderServices[input.provider] - await service.disconnect(context.workspace.id) + const connection = await connectionRepository.findByProviderSourceId({ + workspaceId: context.workspace.id, + provider: input.provider, + sourceId: "workspace", + }) + // Idempotent, same as the pre-Connection-domain per-provider + // `disconnect(workspaceId)` this aliases: a no-op when already + // disconnected, not a 404. + if (connection) { + await connectionService.disconnect({ + connectionId: connection.id, + workspaceId: context.workspace.id, + }) + } await aiIntegrationService.invalidateCache( context.workspace.id, diff --git a/apps/builder/src/features/integrations/api/public/crud.ts b/apps/builder/src/features/integrations/api/public/crud.ts index 390f49d634..3114c9d3da 100644 --- a/apps/builder/src/features/integrations/api/public/crud.ts +++ b/apps/builder/src/features/integrations/api/public/crud.ts @@ -16,7 +16,7 @@ import { } from "../../schema/public" import { publicIntegrationResource } from "../../schema/resource" -const workspaceTokenAuthAPI = workspaceTokenAuthAPIForScope("integrations") +const workspaceTokenAuthAPI = workspaceTokenAuthAPIForScope("connections") export const integrationsCrudPublicRouter = { list: workspaceTokenAuthAPI @@ -63,7 +63,8 @@ export const integrationsCrudPublicRouter = { path: "/v1/integrations/status/token-errors", summary: "List channel integrations with a failed token refresh", description: - "Channel integrations whose daily automatic token-refresh last failed — a signal the channel needs a manual reconnect before it silently stops sending or receiving messages.", + "Deprecated — use `GET /v1/connections?status=needs_reauth` (and `status=degraded`) instead. Channel integrations whose daily automatic token-refresh last failed — a signal the channel needs a manual reconnect before it silently stops sending or receiving messages.", + deprecated: true, tags: ["Integrations"], }) .output(listTokenRefreshErrorsResponse) diff --git a/apps/builder/src/features/personas/api/public.ts b/apps/builder/src/features/personas/api/public.ts index 2de52911fe..1080f84c7c 100644 --- a/apps/builder/src/features/personas/api/public.ts +++ b/apps/builder/src/features/personas/api/public.ts @@ -3,7 +3,7 @@ import { workspaceTokenAuthAPIForScope } from "@/orpc" import { listMessengerPersonaOptions } from "../lib/persona-options" import { listMessengerPersonasResponse } from "../schema/query" -const workspaceTokenAuthAPI = workspaceTokenAuthAPIForScope("channels") +const workspaceTokenAuthAPI = workspaceTokenAuthAPIForScope("connections") export const messengerPersonasPublicRouter = { list: workspaceTokenAuthAPI diff --git a/apps/builder/src/features/user-persistent-menus/api/public.ts b/apps/builder/src/features/user-persistent-menus/api/public.ts index eedd046c81..12f699f2cd 100644 --- a/apps/builder/src/features/user-persistent-menus/api/public.ts +++ b/apps/builder/src/features/user-persistent-menus/api/public.ts @@ -20,7 +20,7 @@ import { userPersistentMenuPublicResource, } from "../schema/public" -const workspaceTokenAuthAPI = workspaceTokenAuthAPIForScope("channels") +const workspaceTokenAuthAPI = workspaceTokenAuthAPIForScope("connections") export const userPersistentMenusPublicRouter = { list: workspaceTokenAuthAPI diff --git a/apps/builder/src/features/webhooks/api/public.ts b/apps/builder/src/features/webhooks/api/public.ts index ed23905514..4a9e247a39 100644 --- a/apps/builder/src/features/webhooks/api/public.ts +++ b/apps/builder/src/features/webhooks/api/public.ts @@ -13,7 +13,7 @@ import { workspaceTokenAuthAPIForScope } from "@/orpc" import { conditionSchema } from "../../conditions/schema" import { publicWebhookResource } from "../schema/resource" -const workspaceTokenAuthAPI = workspaceTokenAuthAPIForScope("integrations") +const workspaceTokenAuthAPI = workspaceTokenAuthAPIForScope("connections") const webhookResource = createSelectSchema(webhookModel, { id: z.string(), diff --git a/apps/builder/src/features/workspaces/lib/workspace-token-scopes.ts b/apps/builder/src/features/workspaces/lib/workspace-token-scopes.ts index 819691fa8b..71a3242f1e 100644 --- a/apps/builder/src/features/workspaces/lib/workspace-token-scopes.ts +++ b/apps/builder/src/features/workspaces/lib/workspace-token-scopes.ts @@ -17,12 +17,11 @@ export const workspaceApiTokenScopeRegistry: Record< broadcasts: { labelKey: "fields.tokenScopes.broadcasts", order: 3 }, analytics: { labelKey: "fields.tokenScopes.analytics", order: 4 }, ecommerce: { labelKey: "fields.tokenScopes.ecommerce", order: 5 }, - integrations: { labelKey: "fields.tokenScopes.integrations", order: 6 }, - channels: { labelKey: "fields.tokenScopes.channels", order: 7 }, - minigames: { labelKey: "fields.tokenScopes.minigames", order: 8 }, - appointments: { labelKey: "fields.tokenScopes.appointments", order: 9 }, - media: { labelKey: "fields.tokenScopes.media", order: 10 }, - ads: { labelKey: "fields.tokenScopes.ads", order: 11 }, + connections: { labelKey: "fields.tokenScopes.connections", order: 6 }, + minigames: { labelKey: "fields.tokenScopes.minigames", order: 7 }, + appointments: { labelKey: "fields.tokenScopes.appointments", order: 8 }, + media: { labelKey: "fields.tokenScopes.media", order: 9 }, + ads: { labelKey: "fields.tokenScopes.ads", order: 10 }, } export const orderedWorkspaceApiTokenScopes = ( diff --git a/apps/builder/src/integration.ts b/apps/builder/src/integration.ts index f7b05de077..b2a998c816 100644 --- a/apps/builder/src/integration.ts +++ b/apps/builder/src/integration.ts @@ -1,49 +1,87 @@ -import { integration as integrationActiveCampaign } from "@chatbotx.io/integration-active-campaign" -import { integration as integrationApi } from "@chatbotx.io/integration-api" -import { integration as integrationChatbotx } from "@chatbotx.io/integration-chatbotx" -import { integration as integrationDrip } from "@chatbotx.io/integration-drip" -import { integration as integrationFacebookAds } from "@chatbotx.io/integration-facebook-ads" -import { integration as integrationGetResponse } from "@chatbotx.io/integration-get-response" -import { integration as integrationGoogleCalendar } from "@chatbotx.io/integration-google-calendar" -import { integration as integrationGoogleSheets } from "@chatbotx.io/integration-google-sheets" -import { integration as integrationInstagram } from "@chatbotx.io/integration-instagram" -import { integration as integrationInstagramFacebook } from "@chatbotx.io/integration-instagram-facebook" -import { integration as integrationKlaviyo } from "@chatbotx.io/integration-klaviyo" -import { integration as integrationMailchimp } from "@chatbotx.io/integration-mailchimp" -import { integration as integrationMailerLite } from "@chatbotx.io/integration-mailer-lite" -import { integration as integrationMessenger } from "@chatbotx.io/integration-messenger" -import { integration as integrationMoosend } from "@chatbotx.io/integration-moosend" -import { integration as integrationSendGrid } from "@chatbotx.io/integration-sendgrid" -import { integration as integrationSmtp } from "@chatbotx.io/integration-smtp" -import { integration as integrationTelegram } from "@chatbotx.io/integration-telegram" -import { integration as integrationTiktok } from "@chatbotx.io/integration-tiktok" -import { integration as integrationWebchat } from "@chatbotx.io/integration-webchat" -import { integration as integrationWhatsapp } from "@chatbotx.io/integration-whatsapp" -import { integration as integrationZalo } from "@chatbotx.io/integration-zalo" +import { CONNECTION_REGISTRY } from "@chatbotx.io/connections" +import type { IntegrationType } from "@chatbotx.io/database/partials" +import type { integration as integrationActiveCampaign } from "@chatbotx.io/integration-active-campaign" +import type { integration as integrationApi } from "@chatbotx.io/integration-api" +import type { integration as integrationChatbotx } from "@chatbotx.io/integration-chatbotx" +import type { integration as integrationDrip } from "@chatbotx.io/integration-drip" +import type { integration as integrationFacebookAds } from "@chatbotx.io/integration-facebook-ads" +import type { integration as integrationGetResponse } from "@chatbotx.io/integration-get-response" +import type { integration as integrationGoogleCalendar } from "@chatbotx.io/integration-google-calendar" +import type { integration as integrationGoogleSheets } from "@chatbotx.io/integration-google-sheets" +import type { integration as integrationInstagram } from "@chatbotx.io/integration-instagram" +import type { integration as integrationInstagramFacebook } from "@chatbotx.io/integration-instagram-facebook" +import type { integration as integrationKlaviyo } from "@chatbotx.io/integration-klaviyo" +import type { integration as integrationMailchimp } from "@chatbotx.io/integration-mailchimp" +import type { integration as integrationMailerLite } from "@chatbotx.io/integration-mailer-lite" +import type { integration as integrationMessenger } from "@chatbotx.io/integration-messenger" +import type { integration as integrationMoosend } from "@chatbotx.io/integration-moosend" +import type { integration as integrationSendGrid } from "@chatbotx.io/integration-sendgrid" +import type { integration as integrationSmtp } from "@chatbotx.io/integration-smtp" +import type { integration as integrationTelegram } from "@chatbotx.io/integration-telegram" +import type { integration as integrationTiktok } from "@chatbotx.io/integration-tiktok" +import type { integration as integrationWebchat } from "@chatbotx.io/integration-webchat" +import type { integration as integrationWhatsapp } from "@chatbotx.io/integration-whatsapp" +import type { integration as integrationZalo } from "@chatbotx.io/integration-zalo" +/** + * `CONNECTION_REGISTRY`'s own module-load assertions + * (`packages/connections/src/registry.ts`'s `fromIntegration`) already throw + * if one of these specific providers loses its `connection` block, so a + * missing `.integration` here can only mean a genuine registry bug. + * + * `ConnectionAdapter.integration` is declared as + * `Integration>` (the heterogeneous- + * registry tradeoff documented on that field) — every entry `integrations` + * below reads is registered from the exact same singleton this file used to + * import directly, so casting back to `T` restores each provider's precise + * static type (`runAction` action-name checking, `channels.*` handler + * shapes) rather than letting every caller silently degrade to `any`. + */ +const requireIntegration = (type: IntegrationType): T => { + const integration = CONNECTION_REGISTRY[type]?.integration + if (!integration) { + throw new Error( + `@/integration: CONNECTION_REGISTRY.${type} has no .integration — the connections registry is out of sync with this file.`, + ) + } + return integration as T +} + +/** + * Derives the same 22-key registry `IntegrationKey` consumers have always + * used, but now sourced from `CONNECTION_REGISTRY` — the single exhaustive + * provider registry `@chatbotx.io/connections` builds from each + * `integrations/` package's `Integration` instance. + */ export const integrations = { - api: integrationApi, - whatsapp: integrationWhatsapp, - messenger: integrationMessenger, - instagram: integrationInstagram, - instagramFacebook: integrationInstagramFacebook, - activeCampaign: integrationActiveCampaign, - drip: integrationDrip, - getResponse: integrationGetResponse, - klaviyo: integrationKlaviyo, - mailchimp: integrationMailchimp, - mailerLite: integrationMailerLite, - moosend: integrationMoosend, - googleCalendar: integrationGoogleCalendar, - googleSheets: integrationGoogleSheets, - facebookAds: integrationFacebookAds, - zalo: integrationZalo, - telegram: integrationTelegram, - tiktok: integrationTiktok, - webchat: integrationWebchat, - chatbotx: integrationChatbotx, - smtp: integrationSmtp, - sendGrid: integrationSendGrid, + api: requireIntegration("api"), + whatsapp: requireIntegration("whatsapp"), + messenger: requireIntegration("messenger"), + instagram: requireIntegration("instagram"), + instagramFacebook: + requireIntegration( + "instagramFacebook", + ), + activeCampaign: + requireIntegration("activeCampaign"), + drip: requireIntegration("drip"), + getResponse: requireIntegration("getResponse"), + klaviyo: requireIntegration("klaviyo"), + mailchimp: requireIntegration("mailchimp"), + mailerLite: requireIntegration("mailerLite"), + moosend: requireIntegration("moosend"), + googleCalendar: + requireIntegration("googleCalendar"), + googleSheets: + requireIntegration("googleSheets"), + facebookAds: requireIntegration("facebookAds"), + zalo: requireIntegration("zalo"), + telegram: requireIntegration("telegram"), + tiktok: requireIntegration("tiktok"), + webchat: requireIntegration("webchat"), + chatbotx: requireIntegration("chatbotx"), + smtp: requireIntegration("smtp"), + sendGrid: requireIntegration("sendGrid"), } export type IntegrationKey = keyof typeof integrations diff --git a/apps/builder/src/lib/facebook-pending-auth.ts b/apps/builder/src/lib/facebook-pending-auth.ts deleted file mode 100644 index 9194b53c02..0000000000 --- a/apps/builder/src/lib/facebook-pending-auth.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { encryptedDataSchema, encryptUtils } from "@chatbotx.io/encryption" -import { cookies } from "next/headers" -import { z } from "zod" - -export const FB_MESSENGER_PENDING_AUTH_COOKIE = "fb_messenger_pending_auth" -export const FB_INSTAGRAM_PENDING_AUTH_COOKIE = "fb_instagram_pending_auth" -export const FB_INSTAGRAM_FACEBOOK_PENDING_AUTH_COOKIE = - "fb_instagram_facebook_pending_auth" -export const FB_PENDING_AUTH_MAX_AGE = 600 // seconds — 10 minutes - -/** - * The one cookie-option set every pending-auth write uses (the OAuth callback's - * three channels and the Messenger reuse route), so no site can scope the - * cookie differently from the rest. - * - * `path: "/"` is load-bearing: the per-account connect is an oRPC POST to - * `RPC_ENDPOINT_PATH` (a server action could not run in parallel), and a - * cookie scoped to the picker page — as these were — is simply never sent - * there, so every batch would fail as `sessionExpired`. - * Widening the path does not weaken the cookie: it stays `httpOnly`, - * encrypted, `SameSite=Lax` and 10-minute-lived, and it is only ever read - * server-side by `readPendingAuth`. - * - * Three invariants here are TEST-ENFORCED against Next's real - * `ResponseCookies` (not a mock) in - * `__tests__/facebook-pending-auth.cookie-store.test.ts` — changing any of - * them fails there: - * - * 1. exactly one `Set-Cookie` per cookie name (see `writePendingAuth`); - * 2. `path: "/"`, which RFC 6265 §5.1.4 path-matches `RPC_ENDPOINT_PATH` and - * every `/channels//select` picker route; - * 3. `maxAge` of `FB_PENDING_AUTH_MAX_AGE` (10 minutes), plus `httpOnly`, - * `SameSite=Lax`, and `Secure` in production only. - */ -function pendingAuthCookieOptions() { - return { - httpOnly: true, - secure: process.env.NODE_ENV === "production", - sameSite: "lax", - maxAge: FB_PENDING_AUTH_MAX_AGE, - path: "/", - } as const -} - -/** The minimal cookie surface this module needs — Next's `cookies()` store satisfies it. */ -type PendingAuthCookieStore = { - set: ( - name: string, - value: string, - options: Record, - ) => unknown -} - -/** - * Writes one channel's pending-auth cookie at the root path. Every set site - * goes through here so the options cannot drift. - * - * Deliberately ONE `set` per name: Next's response cookie store is keyed by - * cookie name (`ResponseCookies._parsed.set(name, …)`), so a second `set` — - * e.g. expiring the picker-scoped cookie an older release wrote under the - * same name — would silently REPLACE the real cookie and the picker would - * see no session at all. A leftover picker-scoped cookie from before the - * root-path change therefore simply ages out on its own 10-minute `maxAge`. - * - * That collapse is not an argument about Next's source: the cookie-store test - * writes two `set()` calls into a real `ResponseCookies` and asserts the - * `Headers` end up with ONE `Set-Cookie` — so re-introducing a second write - * here fails immediately. - */ -export function writePendingAuth( - cookieStore: PendingAuthCookieStore, - cookieName: string, - token: string, -): void { - cookieStore.set(cookieName, token, pendingAuthCookieOptions()) -} - -/** - * Shape of the decrypted pending-auth cookie payload the OAuth callback - * stores for the Messenger/Instagram/Instagram-via-Facebook pickers, and - * every per-account connect action's ONLY source of the user token and - * `workspaceId` (never client input — plan §4.7). `readPendingAuth` parses - * the decrypted JSON with this schema instead of blindly casting it, so a - * tampered or stale-shape payload is treated the same as a missing cookie. - */ -export const facebookAuthCallbackSchema = z.object({ - userToken: z.string().min(1), - /** Graph identity of the authorizing user; absent when the lookup failed. */ - userId: z.string().optional(), - userName: z.string().optional(), - /** Provider-hosted profile picture URL (not yet uploaded to storage). */ - userAvatarUrl: z.string().optional(), - workspaceId: z.string().min(1), - referer: z.string().min(1), - version: z.string().min(1), - expiresAt: z.number(), -}) -export type FacebookAuthCallback = z.infer - -export async function encryptAuth(data: unknown): Promise { - const encrypted = await encryptUtils.encryptObject(data) - return Buffer.from(JSON.stringify(encrypted)).toString("base64url") -} - -/** Decrypts the base64url-wrapped envelope down to its raw JSON payload; null on any tamper/decrypt failure. */ -async function decryptToRawPayload(token: string): Promise { - try { - const raw = JSON.parse(Buffer.from(token, "base64url").toString()) - const encrypted = encryptedDataSchema.parse(raw) - const text = await encryptUtils.decryptText(encrypted) - return JSON.parse(text) - } catch { - return null - } -} - -/** - * Read and decrypt the pending-auth cookie for a channel; null if missing, - * expired, tampered, OR schema-invalid. Every picker page and per-account - * connect action uses this so a malformed payload — a required field - * renamed/dropped by a future change, or an outright forged cookie — is - * treated the same as no session at all instead of reaching the caller with - * `undefined` fields. - */ -export async function readPendingAuth( - cookieName: string, -): Promise { - const token = (await cookies()).get(cookieName)?.value - if (!token) { - return null - } - const raw = await decryptToRawPayload(token) - const parsed = facebookAuthCallbackSchema.safeParse(raw) - if (!parsed.success || Date.now() > parsed.data.expiresAt) { - return null - } - return parsed.data -} diff --git a/apps/builder/src/lib/integration-actions.ts b/apps/builder/src/lib/integration-actions.ts index 18c5503120..7c79ebe9fc 100644 --- a/apps/builder/src/lib/integration-actions.ts +++ b/apps/builder/src/lib/integration-actions.ts @@ -1,3 +1,6 @@ +import { connectionService } from "@chatbotx.io/connections" +import type { IntegrationType } from "@chatbotx.io/database/partials" +import { connectionRepository } from "@chatbotx.io/database/repositories" import { normalizeError } from "universal-error-normalizer" import { type WorkspaceIdRequestParams, @@ -17,13 +20,32 @@ interface CreateDisconnectActionOptions { log?: boolean /** Human-readable integration name for the error log, e.g. "ActiveCampaign". */ name: string + /** + * The `Connection` registry key for this integration — workspace-level + * integrations are singletons (`sourceId = "workspace"`). When a + * `Connection` row exists for `(workspaceId, provider, "workspace")` the + * disconnect routes through `connectionService.disconnect` (provider-side + * teardown + store-binding delete + FSM transition); otherwise (a + * workspace predating the Phase 1 backfill) it falls back to `service`'s + * own `disconnect`, so this never regresses a not-yet-backfilled + * workspace's ability to disconnect. + */ + provider: IntegrationType } +/** + * Scheduled for removal in Phase 5 of the connection-lifecycle plan, once + * every adopter reads/writes the `Connection` domain directly instead of + * going through a per-provider service. Until then this is the active, + * correct implementation for the 13 workspace-integration disconnect + * actions — it routes through `connectionService` itself (see `provider` + * above), so adopters do not need any further change. + */ export function createDisconnectAction( service: DisconnectService, options: CreateDisconnectActionOptions, ) { - const { name, log = true, afterDisconnect } = options + const { name, log = true, afterDisconnect, provider } = options return workspaceActionClientAllowExpired .bindArgsSchemas(workspaceIdrequestParams) @@ -34,7 +56,19 @@ export function createDisconnectAction( bindArgsParsedInputs: WorkspaceIdRequestParams }) => { try { - await service.disconnect(workspaceId) + const connection = await connectionRepository.findByProviderSourceId({ + workspaceId, + provider, + sourceId: "workspace", + }) + if (connection) { + await connectionService.disconnect({ + connectionId: connection.id, + workspaceId, + }) + } else { + await service.disconnect(workspaceId) + } await afterDisconnect?.(workspaceId) } catch (error) { if (log) { diff --git a/apps/builder/src/lib/oauth-referer.ts b/apps/builder/src/lib/oauth-referer.ts index e92d912e20..adc1522828 100644 --- a/apps/builder/src/lib/oauth-referer.ts +++ b/apps/builder/src/lib/oauth-referer.ts @@ -37,6 +37,29 @@ export async function sanitizeReferer(referer: string): Promise { } } +/** + * Validates an optional client-supplied `redirectUrl` (e.g. `POST + * /v1/connections {redirectUrl}`) against the same allow-list as + * `sanitizeReferer`, but returns `undefined` instead of falling back to the + * builder-internal `/manage` path — an unattended API/MCP caller has no + * builder browser session for that fallback to mean anything. A missing or + * disallowed value leaves `ConnectSession.returnUrl` unset, so the + * completion page shows its own neutral "you may close this window" default. + */ +export async function sanitizeOptionalReturnUrl( + redirectUrl: string | undefined, +): Promise { + if (!redirectUrl) { + return + } + try { + const url = new URL(redirectUrl) + return (await isAllowedOrigin(url)) ? redirectUrl : undefined + } catch { + return + } +} + /** * OAuth redirect_uris are pinned per-credential: the broker host for * inherited/platform credentials, or the reseller's own custom domain for a diff --git a/apps/builder/src/lib/orpc/orpc-error-helper.ts b/apps/builder/src/lib/orpc/orpc-error-helper.ts index 0a41989b3f..237bcc8dcf 100644 --- a/apps/builder/src/lib/orpc/orpc-error-helper.ts +++ b/apps/builder/src/lib/orpc/orpc-error-helper.ts @@ -284,3 +284,118 @@ export const possibleErrorsOnMutatingEmailTopic = { businessError, nameTaken, } satisfies ErrorMap + +/** + * `refresh`/`verify` on a `Connection` whose status is not `connected`/ + * `degraded` throws `connectionInactive` (409) — see `ConnectionService` in + * `@chatbotx.io/connections`. + */ +const connectionInactive = { + message: "This connection is not active", + status: 409, +} + +/** No `ConnectionAdapter`/store binding registered for the provider — a data-integrity gap, not a user error. */ +const connectionNotConfigured = { + message: "This connection provider is not configured", + status: 500, +} + +/** The provider has no `refreshAuth` handler (e.g. a static API-key credential). */ +const connectionNotRefreshable = { + message: "This connection provider does not support refresh", + status: 400, +} + +export const possibleErrorsOnDisconnectingConnection = { + notFound, + businessError, +} satisfies ErrorMap + +export const possibleErrorsOnRefreshingConnection = { + notFound, + connectionInactive, + connectionNotConfigured, + connectionNotRefreshable, +} satisfies ErrorMap + +export const possibleErrorsOnVerifyingConnection = { + notFound, + businessError, + connectionInactive, + connectionNotConfigured, +} satisfies ErrorMap + +/** `connectFromCredentials`'s config-validation/live-check failures — see `ConnectionService` in `@chatbotx.io/connections`. */ +const connectionWrongStrategy = { + message: "This connection provider does not accept direct credentials", + status: 400, +} + +const connectionCredentialsRejected = { + message: "The provided credentials were rejected", + status: 400, +} + +const connectionNotOAuth = { + message: "This connection provider does not support an OAuth connect flow", + status: 400, +} + +/** The tenant's channel-visibility policy hides this channel from an unattended API caller — see `channelHiddenException`. */ +const channelHidden = { + message: "This channel is not available for this workspace", + status: 403, +} + +export const possibleErrorsOnCreatingConnection = { + businessError, + connectionAlreadyConnected: { + message: "This provider is already connected in this workspace", + status: 409, + }, + connectionWrongStrategy, + connectionCredentialsRejected, + connectionNotOAuth, + connectionNotConfigured, + channelHidden, +} satisfies ErrorMap + +export const possibleErrorsOnReconnectingConnection = { + notFound, + businessError, + connectionNotOAuth, + connectionNotConfigured, +} satisfies ErrorMap + +export const possibleErrorsOnUpdatingConnection = { + notFound, + businessError, +} satisfies ErrorMap + +/** The `state` nonce did not resolve to a matching session, or the session is expired/consumed — see `ConnectSessionService`. */ +const connectSessionExpired = { + message: "This connect session is no longer active", + status: 400, +} + +export const possibleErrorsOnFindingConnectSession = { + notFound, + businessError, +} satisfies ErrorMap + +export const possibleErrorsOnConnectingSessionTargets = { + notFound, + businessError, + connectSessionExpired, +} satisfies ErrorMap + +export const possibleErrorsOnSubmittingConnectSessionInput = { + notFound, + connectSessionExpired, +} satisfies ErrorMap + +export const possibleErrorsOnCancelingConnectSession = { + notFound, + businessError, +} satisfies ErrorMap diff --git a/apps/builder/src/routers/index.ts b/apps/builder/src/routers/index.ts index ed0c9e6dc4..8ada9f3e62 100644 --- a/apps/builder/src/routers/index.ts +++ b/apps/builder/src/routers/index.ts @@ -60,6 +60,16 @@ export const router = { default: m.dynamicImagesAPI, })), ), + connectionsAPI: lazy(() => + import("@/features/connections/api").then((m) => ({ + default: m.connectionsAPI, + })), + ), + connectSessionsAPI: lazy(() => + import("@/features/connections/api").then((m) => ({ + default: m.connectSessionsAPI, + })), + ), emailTopicsAPI: lazy(() => import("@/features/email-topics/api").then((m) => ({ default: m.emailTopicsAPI, diff --git a/apps/builder/src/routers/public.ts b/apps/builder/src/routers/public.ts index c3dc98be0b..c67abf9262 100644 --- a/apps/builder/src/routers/public.ts +++ b/apps/builder/src/routers/public.ts @@ -11,6 +11,11 @@ import { appointmentsPublicRouter } from "@/features/appointments/api/public" import { keywordsPublicRouter } from "@/features/automated-response/api/public" import { botFieldsPublicRouter } from "@/features/bot-fields/api/public" import { broadcastsPublicRouter } from "@/features/broadcasts/api/public" +import { + connectionProvidersPublicRouter, + connectionsPublicRouter, + connectSessionsPublicRouter, +} from "@/features/connections/api/public" import { contactScanPublicRouter } from "@/features/contact-scan/api/public" import { contactsPublicRouter } from "@/features/contacts/api/public" import { conversationsPublicRouter } from "@/features/conversations/api/public" @@ -67,6 +72,9 @@ export const publicRouter = { botFields: botFieldsPublicRouter, broadcasts: broadcastsPublicRouter, channels: channelsPublicRouter, + connectionProviders: connectionProvidersPublicRouter, + connections: connectionsPublicRouter, + connectSessions: connectSessionsPublicRouter, contactScans: contactScanPublicRouter, contacts: contactsPublicRouter, conversations: conversationsPublicRouter, diff --git a/apps/worker/__tests__/purge-expired-connect-sessions.test.ts b/apps/worker/__tests__/purge-expired-connect-sessions.test.ts new file mode 100644 index 0000000000..00f2585767 --- /dev/null +++ b/apps/worker/__tests__/purge-expired-connect-sessions.test.ts @@ -0,0 +1,42 @@ +import { beforeEach, describe, expect, test, vi } from "vitest" + +const purgeExpired = vi.fn() +const info = vi.fn() + +vi.mock("@chatbotx.io/business/connect-session", () => ({ + connectSessionService: { purgeExpired }, +})) +vi.mock("@chatbotx.io/logger", () => ({ + getChildLogger: () => ({ info }), +})) + +const { purgeExpiredConnectSessions } = await import( + "../src/schedule/handlers/purge-expired-connect-sessions" +) + +beforeEach(() => { + purgeExpired.mockReset() + info.mockReset() +}) + +describe("purgeExpiredConnectSessions", () => { + test("logs the count when sessions were purged", async () => { + purgeExpired.mockResolvedValue(3) + + await purgeExpiredConnectSessions() + + expect(purgeExpired).toHaveBeenCalledOnce() + expect(info).toHaveBeenCalledWith( + { purged: 3 }, + "purgeExpiredConnectSessions: sessions expired", + ) + }) + + test("does not log when nothing was purged", async () => { + purgeExpired.mockResolvedValue(0) + + await purgeExpiredConnectSessions() + + expect(info).not.toHaveBeenCalled() + }) +}) diff --git a/apps/worker/package.json b/apps/worker/package.json index f58bb74a30..7f1fb2b93e 100644 --- a/apps/worker/package.json +++ b/apps/worker/package.json @@ -36,6 +36,7 @@ "@chatbotx.io/analytics": "workspace:*", "@chatbotx.io/automated-response": "workspace:*", "@chatbotx.io/business": "workspace:*", + "@chatbotx.io/connections": "workspace:*", "@chatbotx.io/database": "workspace:*", "@chatbotx.io/encryption": "workspace:*", "@chatbotx.io/event-bus": "workspace:*", diff --git a/apps/worker/src/schedule/handlers/purge-expired-connect-sessions.ts b/apps/worker/src/schedule/handlers/purge-expired-connect-sessions.ts new file mode 100644 index 0000000000..d35eb028ba --- /dev/null +++ b/apps/worker/src/schedule/handlers/purge-expired-connect-sessions.ts @@ -0,0 +1,20 @@ +import { connectSessionService } from "@chatbotx.io/business/connect-session" +import { getChildLogger } from "@chatbotx.io/logger" + +const log = getChildLogger("purge-expired-connect-sessions") + +/** + * Flips every `ConnectSession` past `expiresAt` to `status = "expired"`. + * Every reader already treats a past-`expiresAt` row as expired regardless + * of its stored status (`ConnectSessionService`'s lazy `applyExpiryRule`), + * so this is display/reporting hygiene and the `countActiveByWorkspaceId` + * pending-session cap, not a correctness dependency — but without it, + * abandoned sessions (and the `encryptedAuth` they carry) accumulate at + * rest indefinitely instead of reading as terminal. + */ +export async function purgeExpiredConnectSessions(): Promise { + const purged = await connectSessionService.purgeExpired() + if (purged > 0) { + log.info({ purged }, "purgeExpiredConnectSessions: sessions expired") + } +} diff --git a/apps/worker/src/schedule/handlers/register-schedules.ts b/apps/worker/src/schedule/handlers/register-schedules.ts index e9e5cd276b..288787675f 100644 --- a/apps/worker/src/schedule/handlers/register-schedules.ts +++ b/apps/worker/src/schedule/handlers/register-schedules.ts @@ -300,6 +300,20 @@ export const registerSchedules = async () => { }, ) + await scheduleQueue.upsertJobScheduler( + ScheduleJobData.purgeExpiredConnectSessions, + { + pattern: "0 * * * *", + }, + { + name: ScheduleJobData.purgeExpiredConnectSessions, + data: { + type: ScheduleJobData.purgeExpiredConnectSessions, + data: {}, + }, + }, + ) + await scheduleQueue.upsertJobScheduler( ScheduleJobData.purgeWorkspaces, { diff --git a/apps/worker/src/schedule/worker.ts b/apps/worker/src/schedule/worker.ts index 4f13d1c288..423a5aa6b9 100644 --- a/apps/worker/src/schedule/worker.ts +++ b/apps/worker/src/schedule/worker.ts @@ -28,6 +28,7 @@ import { purgeBroadcasts } from "./handlers/purge-broadcasts" import { purgeCoexistStaging } from "./handlers/purge-coexist-staging" import { purgeCommentAutomationEvents } from "./handlers/purge-comment-automation-events" import { purgeErrorLogs } from "./handlers/purge-error-logs" +import { purgeExpiredConnectSessions } from "./handlers/purge-expired-connect-sessions" import { purgeWhatsappSignupSessions } from "./handlers/purge-whatsapp-signup-sessions" import { purgeWorkspaces } from "./handlers/purge-workspaces" import { reconcileBroadcasts } from "./handlers/reconcile-broadcasts" @@ -145,6 +146,10 @@ async function startScheduleWorker() { await purgeWhatsappSignupSessions() return + case ScheduleJobData.purgeExpiredConnectSessions: + await purgeExpiredConnectSessions() + return + case ScheduleJobData.purgeWorkspaces: await purgeWorkspaces() return diff --git a/apps/worker/src/services/integrations.ts b/apps/worker/src/services/integrations.ts index 8b585ea791..b81cb6d922 100644 --- a/apps/worker/src/services/integrations.ts +++ b/apps/worker/src/services/integrations.ts @@ -3,6 +3,7 @@ import { type IntegrationContext, workspaceService, } from "@chatbotx.io/business" +import { CONNECTION_REGISTRY } from "@chatbotx.io/connections" import { findOrFail } from "@chatbotx.io/database/client" import type { IntegrationType } from "@chatbotx.io/database/partials" import { integrationLookupRepository } from "@chatbotx.io/database/repositories" @@ -12,19 +13,6 @@ import type { InboxModel, WorkspaceModel, } from "@chatbotx.io/database/types" -import { integration as integrationApi } from "@chatbotx.io/integration-api" -import { integration as integrationChatbotx } from "@chatbotx.io/integration-chatbotx" -import { integration as integrationGoogleCalendar } from "@chatbotx.io/integration-google-calendar" -import { integration as integrationGoogleSheets } from "@chatbotx.io/integration-google-sheets" -import { integration as integrationInstagram } from "@chatbotx.io/integration-instagram" -import { integration as integrationInstagramFacebook } from "@chatbotx.io/integration-instagram-facebook" -import { integration as integrationMessenger } from "@chatbotx.io/integration-messenger" -import { integration as integrationSmtp } from "@chatbotx.io/integration-smtp" -import { integration as integrationTelegram } from "@chatbotx.io/integration-telegram" -import { integration as integrationTiktok } from "@chatbotx.io/integration-tiktok" -import { integration as integrationWebchat } from "@chatbotx.io/integration-webchat" -import { integration as integrationWhatsapp } from "@chatbotx.io/integration-whatsapp" -import { integration as integrationZalo } from "@chatbotx.io/integration-zalo" import { type AuthValue, ChannelError, @@ -35,26 +23,34 @@ import { } from "@chatbotx.io/sdk" import { IntegrationNotFoundError } from "./orphaned-integration-cleanup" +/** + * Sourced from `CONNECTION_REGISTRY` (the single exhaustive provider + * registry `@chatbotx.io/connections` builds) instead of importing each + * `integrations/` package directly. `gemini`/`openai` stay `undefined` + * — same as before this derivation — because those two `IntegrationType`s + * have no `integrations/` SDK package (`CONNECTION_REGISTRY.gemini`/`.openai` + * are credential-only adapters with no `.integration` field to read). + */ export const allIntegrations: Record< string, // biome-ignore lint/suspicious/noExplicitAny: safe pass value Integration> | undefined > = { - api: integrationApi, - gemini: undefined, - googleCalendar: integrationGoogleCalendar, - googleSheets: integrationGoogleSheets, - messenger: integrationMessenger, - openai: undefined, - webchat: integrationWebchat, - whatsapp: integrationWhatsapp, - telegram: integrationTelegram, - tiktok: integrationTiktok, - zalo: integrationZalo, - chatbotx: integrationChatbotx, - smtp: integrationSmtp, - instagram: integrationInstagram, - instagramFacebook: integrationInstagramFacebook, + api: CONNECTION_REGISTRY.api?.integration, + gemini: CONNECTION_REGISTRY.gemini?.integration, + googleCalendar: CONNECTION_REGISTRY.googleCalendar?.integration, + googleSheets: CONNECTION_REGISTRY.googleSheets?.integration, + messenger: CONNECTION_REGISTRY.messenger?.integration, + openai: CONNECTION_REGISTRY.openai?.integration, + webchat: CONNECTION_REGISTRY.webchat?.integration, + whatsapp: CONNECTION_REGISTRY.whatsapp?.integration, + telegram: CONNECTION_REGISTRY.telegram?.integration, + tiktok: CONNECTION_REGISTRY.tiktok?.integration, + zalo: CONNECTION_REGISTRY.zalo?.integration, + chatbotx: CONNECTION_REGISTRY.chatbotx?.integration, + smtp: CONNECTION_REGISTRY.smtp?.integration, + instagram: CONNECTION_REGISTRY.instagram?.integration, + instagramFacebook: CONNECTION_REGISTRY.instagramFacebook?.integration, } export type IntegrationRow = { diff --git a/docs/developer/workspace-api-tokens.md b/docs/developer/workspace-api-tokens.md index 2caf52539c..7ba13397e6 100644 --- a/docs/developer/workspace-api-tokens.md +++ b/docs/developer/workspace-api-tokens.md @@ -134,7 +134,7 @@ the authoritative, current list, and each feature's scope assignment at compile/test time (e.g. `contacts-public-scope.test.ts`, `broadcasts-public-scope.test.ts`, `appointments-public-scope.test.ts`, `sequences-public-scope.test.ts`, -`integrations-public-scope.test.ts`, `analytics-public-scope.test.ts`, +`connections-public-scope.test.ts`, `analytics-public-scope.test.ts`, `conversations-public-scope.test.ts`, `products-public-scope.test.ts`, `product-categories-public-scope.test.ts`, `coupons-public-scope.test.ts`). @@ -327,18 +327,21 @@ an endpoint's scope. — the same template the builder's edit page shows the user. Never publish the bare `backgroundUrl` column value. -- **Channels** — see the dedicated table below. +- **Connections** — see the dedicated table below. -### Channels scope — endpoint-to-scope table +### Connections scope — endpoint-to-scope table -`channels` shipped in the enum/registry/i18n alongside `ads` but, like `ads`, -carried no endpoints for a while. It now covers user persistent menus -(Messenger bot menu) CRUD, webchat CRUD, SMTP integration CRUD, -Messenger/Zalo tag-sync toggling, and a read-only list of Messenger personas -across the workspace's connected Pages. As with every other scope, each -public handler calls the same `packages/business` service method the -private/action code calls — no business logic was duplicated to publish -these. +`connections` replaces the former `channels` and `integrations` scopes +(merged 2026-09-14; a data migration rewrites already-issued tokens' +`scopes` arrays from either old value to `connections`). It covers user +persistent menus (Messenger bot menu) CRUD, webchat CRUD, SMTP integration +CRUD, Messenger/Zalo tag-sync toggling, a read-only list of Messenger +personas across the workspace's connected Pages, generic integration +list/get (including the token-refresh-error signal), AI-provider-key +connect/disconnect, and webhook/external-webhook CRUD. As with every other +scope, each public handler calls the same `packages/business` service +method the private/action code calls — no business logic was duplicated to +publish these. | Endpoint | Notes | |---|---| @@ -348,10 +351,17 @@ these. | `PATCH /v1/messenger-channels/{id}/tag-sync` | Toggles `syncTagEnabledAt` via `messengerIntegrationService.updateTagSync`. | | `PATCH /v1/zalo-channels/{id}/tag-sync` | Toggles `syncTagEnabledAt` via `zaloIntegrationService.updateTagSync`. | | `GET /v1/messenger-personas` | Read-only; lists Messenger personas across every Page connected to the workspace, with page access tokens projected away. | +| `GET /v1/integrations`, `GET /v1/integrations/{id}` | Read-only list/get via `integrationService`. | +| `GET /v1/integrations/status/token-errors` | Channel integrations whose daily automatic token-refresh last failed. Superseded by the Connection domain's `needs_reauth`/`degraded` status (not yet public in this branch) — will be marked `deprecated: true` once its `/v1/connections` equivalent ships. | +| `GET/PUT/DELETE /v1/integrations/ai/{provider}` | Get/upsert/disconnect an AI-provider API key (`claude`, `deepseek`, `gemini`, `openai`) via `integrationService`. `PUT` live-validates the key with `verifyAiProviderApiKey` before persisting. | +| `GET/POST /v1/webhooks`, `DELETE /v1/webhooks/{id}` | Full CRUD via `webhookService`. | +| `GET/POST /v1/external-webhooks`, `DELETE /v1/external-webhooks/{id}` | Full CRUD via `externalWebhookService`, scoped to platforms like Make; `POST` is idempotent on `(event, url)`. | +| `GET /v1/connections`, `GET /v1/connections/{id}` | Read-only, `ORDER BY kind, provider, displayName, id`, via `connectionStateService`. The unified successor to the four rows above and `GET /v1/integrations` — not a replacement yet (nothing is marked `deprecated: true` until its Phase 2/3 write-path equivalent ships), but the shared read path both the public and private (`GET /workspaces/{workspaceId}/connections`) routes call. `capabilities` (`refreshable`/`verifiable`/`multiAccount`) is joined from `CONNECTION_REGISTRY` at response time — never stored on the row. `auth` is never returned. | +| `GET /v1/connection-providers` | Read-only connect catalog — every `IntegrationType`'s `strategy`, `configFields` (labels resolved from the request locale, falling back to the raw field name for a key with no translation yet), and `available`/`unavailableReason` (`notImplemented` \| `hiddenForTenant` \| `alreadyConnected` \| `credentialMissing`) resolved against this workspace via `resolveChannelPolicy` + `platformCredentialService.resolveForOwner`. No `POST /v1/connections` yet — Phase 3. | Two invariants specific to this scope: -- **`customCss` is writable by a `channels`-scoped token with no extra +- **`customCss` is writable by a `connections`-scoped token with no extra permission check.** The private `updateWebchatAction` gates `customCss` behind `hasWorkspacePermission(..., "superAdmin")` because it renders via `dangerouslySetInnerHTML` in `lib/widget-css.tsx`. The public webchat @@ -403,7 +413,7 @@ Two invariants specific to this scope: ### Ads scope — endpoint-to-scope table -`ads` shipped in the enum/registry/i18n from day one (alongside `channels`, +`ads` shipped in the enum/registry/i18n from day one (alongside `connections`, `minigames`, `appointments`, `media`) but carried no endpoints until this table's routes were added — a token scoped to `["ads"]` reached nothing before. It now covers Ads conversion-rule CRUD, the CTWA/CTM/CTID funnel and @@ -500,6 +510,10 @@ these helpers — import from the business package directly. asserts a campaign mutation succeeds with no session user in context (the `assertWorkspaceSuperAdmin` regression guard) and that `createdBy` is never set from one +- `apps/builder/__tests__/connections-public-scope.test.ts` — real-router scope wiring for the merged `connections` scope (10 submodules) +- `apps/builder/__tests__/connections-public-api.test.ts` — handler-behavior tests for `GET /v1/connections`, `GET /v1/connections/{id}`, `GET /v1/connection-providers` +- `packages/business/src/connection/__tests__/state-service.test.ts` — quota-edge-exactly-once and Inbox-mirror assertions for `ConnectionStateService.transition`/`markUnhealthy` +- `packages/connections/__tests__/registry.test.ts` — `CONNECTION_REGISTRY` exhaustiveness and channel/credential invariants - `apps/builder/__tests__/create-workspace-token-action.test.ts` - `apps/builder/__tests__/delete-workspace-token-action.test.ts` - `apps/builder/__tests__/integration-api-token-hash.test.ts` diff --git a/integrations/active-campaign/src/integration.ts b/integrations/active-campaign/src/integration.ts index d2e1503f54..40dd6bf00d 100644 --- a/integrations/active-campaign/src/integration.ts +++ b/integrations/active-campaign/src/integration.ts @@ -73,6 +73,51 @@ const config: IntegrationDefinition< ActiveCampaignActions > = { name: "activeCampaign", + connection: { + kind: "integration", + strategy: "api_key", + multiAccount: false, + configFields: [ + { + name: "apiUrl", + type: "url", + required: true, + labelKey: "integrations.activeCampaign.fields.apiUrl", + }, + { + name: "apiKey", + type: "secret", + required: true, + labelKey: "integrations.activeCampaign.fields.apiKey", + }, + ], + describe: () => ({ + // ActiveCampaign auth has no stable account id; this is workspace-singleton. + sourceId: "workspace", + displayName: "ActiveCampaign", + }), + verify: async ({ auth }) => { + try { + await activeCampaignRequest( + auth, + activeCampaignAccountsPath(), + activeCampaignAccountsResponseSchema, + ) + return { ok: true } + } catch (error) { + return { + ok: false, + revoked: false, + error: + error instanceof Error + ? error.message + : "Unable to verify ActiveCampaign credentials", + } + } + }, + // TODO(connection-phase2): refine once ActiveCampaign revoked-token error shape is confirmed. + isRevokedTokenError: () => false, + }, actions: { validateCredentials: async ({ props }) => { const credential = activeCampaignCredentialSchema.parse(props) diff --git a/integrations/api/src/integration.ts b/integrations/api/src/integration.ts index 5939359a49..4a34e55cce 100644 --- a/integrations/api/src/integration.ts +++ b/integrations/api/src/integration.ts @@ -18,6 +18,20 @@ const config: IntegrationDefinition = { }, }, actions: {}, + connection: { + kind: "channel", + strategy: "self_serve", + multiAccount: true, + configFields: [], + describe: () => ({ + // The API channel has no external account identity. + sourceId: "workspace", + displayName: "API channel", + }), + // The API channel has no external provider to verify. + verify: async () => ({ ok: true }), + isRevokedTokenError: () => false, + }, handleRequest( _props: HandleRequestProps, ): Promise { diff --git a/integrations/chatbotx/src/integration.ts b/integrations/chatbotx/src/integration.ts index 2d50d76393..2fe5316ef8 100644 --- a/integrations/chatbotx/src/integration.ts +++ b/integrations/chatbotx/src/integration.ts @@ -15,6 +15,20 @@ const config: IntegrationDefinition = { }, }, actions: {}, + connection: { + kind: "channel", + strategy: "self_serve", + multiAccount: false, + configFields: [], + describe: () => ({ + // ChatbotX is a built-in workspace channel with no external account. + sourceId: "workspace", + displayName: "ChatbotX", + }), + // ChatbotX is internal, so there is no external provider to verify. + verify: async () => ({ ok: true }), + isRevokedTokenError: () => false, + }, handleRequest( _props: HandleRequestProps, ): Promise { diff --git a/integrations/drip/src/integration.ts b/integrations/drip/src/integration.ts index 70afa57675..caa8147046 100644 --- a/integrations/drip/src/integration.ts +++ b/integrations/drip/src/integration.ts @@ -25,6 +25,48 @@ import { const config: IntegrationDefinition = { name: "drip", + connection: { + kind: "integration", + strategy: "api_key", + multiAccount: false, + configFields: [ + { + name: "apiToken", + type: "secret", + required: true, + labelKey: "integrations.drip.fields.apiToken", + }, + ], + describe: () => ({ + // Drip auth has no stable account id; this is workspace-singleton. + sourceId: "workspace", + displayName: "Drip", + }), + verify: async ({ auth }) => { + try { + const response = await dripRequest( + auth, + DRIP_ACCOUNTS_PATH, + dripAccountsResponseSchema, + ) + if (response.accounts.length === 0) { + throw new DripNoAccountError() + } + return { ok: true } + } catch (error) { + return { + ok: false, + revoked: false, + error: + error instanceof Error + ? error.message + : "Unable to verify Drip credentials", + } + } + }, + // TODO(connection-phase2): refine once Drip revoked-token error shape is confirmed. + isRevokedTokenError: () => false, + }, actions: { validateCredentials: async ({ props }) => { const response = await dripRequest( diff --git a/integrations/facebook-ads/__tests__/connection-oauth.test.ts b/integrations/facebook-ads/__tests__/connection-oauth.test.ts new file mode 100644 index 0000000000..437cf74737 --- /dev/null +++ b/integrations/facebook-ads/__tests__/connection-oauth.test.ts @@ -0,0 +1,91 @@ +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest" + +const mocks = vi.hoisted(() => ({ + exchangeCodeForToken: vi.fn(), + exchangeLongLivedToken: vi.fn(), +})) + +vi.mock("../src/apis/auth", async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + exchangeCodeForToken: mocks.exchangeCodeForToken, + exchangeLongLivedToken: mocks.exchangeLongLivedToken, + } +}) + +const { integration } = await import("../src/integration") + +const connection = integration.connection +if (!connection) { + throw new Error("Facebook Ads connection provider is not configured") +} + +const credential = { + clientId: "client-1", + clientSecret: "secret-1", + version: "v23.0", +} + +describe("Facebook Ads connection.authorizeUrl", () => { + test("passes state through verbatim, not JSON/base64-wrapped", () => { + const url = connection.authorizeUrl?.({ + credential, + callbackUrl: + "https://app.example.test/integrations/facebook-ads/callback", + state: "session-1.abc123", + }) + const parsed = new URL(url as string) + + expect(parsed.searchParams.get("state")).toBe("session-1.abc123") + expect(parsed.searchParams.get("client_id")).toBe("client-1") + expect(parsed.searchParams.get("redirect_uri")).toBe( + "https://app.example.test/integrations/facebook-ads/callback", + ) + expect(parsed.searchParams.get("scope")).toBe( + "ads_read,ads_management,pages_manage_ads,pages_read_engagement,pages_show_list", + ) + expect(parsed.hostname).toBe("www.facebook.com") + }) +}) + +describe("Facebook Ads connection.exchangeCode", () => { + beforeEach(() => { + vi.clearAllMocks() + mocks.exchangeCodeForToken.mockResolvedValue("short-lived-token") + mocks.exchangeLongLivedToken.mockResolvedValue({ + accessToken: "long-lived-token", + expiresIn: 3600, + }) + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + test("exchanges short-lived code token for a complete custom AuthValue", async () => { + vi.spyOn(Date, "now").mockReturnValue(1000) + + const auth = await connection.exchangeCode?.({ + code: "auth-code", + callbackUrl: "https://app.example.test/callback", + credential, + }) + + expect(mocks.exchangeCodeForToken).toHaveBeenCalledWith( + credential, + "auth-code", + "https://app.example.test/callback", + ) + expect(mocks.exchangeLongLivedToken).toHaveBeenCalledWith( + credential, + "short-lived-token", + ) + expect(auth).toEqual({ + authType: "custom", + accessToken: "long-lived-token", + version: "v23.0", + expiresAt: "1970-01-01T01:00:01.000Z", + }) + }) +}) diff --git a/integrations/facebook-ads/src/integration.ts b/integrations/facebook-ads/src/integration.ts index 0067cfa0a4..62b6408004 100644 --- a/integrations/facebook-ads/src/integration.ts +++ b/integrations/facebook-ads/src/integration.ts @@ -1,4 +1,5 @@ import { + AuthType, Integration, type IntegrationDefinition, SdkException, @@ -20,7 +21,11 @@ import { getAudienceMarketingMessagesPage, mutateAudienceUsers, } from "./apis/audience-users" -import { revokeToken } from "./apis/auth" +import { + exchangeCodeForToken, + exchangeLongLivedToken, + revokeToken, +} from "./apis/auth" import { createCampaign, getCampaign, @@ -29,7 +34,8 @@ import { } from "./apis/campaigns" import { createCustomAudience } from "./apis/custom-audiences" import { getAdInsights, getMessagingAdsInsightsByAdIds } from "./apis/insights" -import { FacebookAdsException } from "./exception" +import { DEFAULT_API_VERSION, FACEBOOK_ADS_SCOPES } from "./constants" +import { FacebookAdsException, getGraphErrorCode } from "./exception" import type { FacebookAdsActions, FacebookAdsAuthValue, @@ -42,6 +48,58 @@ const config: IntegrationDefinition< FacebookAdsActions > = { name: "facebookAds", + connection: { + kind: "integration", + strategy: "oauth_redirect", + multiAccount: false, + configFields: [], + // Bypasses `generateAdsAuthUrl` deliberately: that legacy helper + // base64-JSON-encodes state for its cookie flow, while the Connection + // domain callback hub matches the raw `"{sessionId}.{nonce}"` string. + authorizeUrl: ({ credential, callbackUrl, state }) => { + const config = credential as FacebookAdsConfig + const params = new URLSearchParams({ + client_id: config.clientId, + redirect_uri: callbackUrl, + scope: FACEBOOK_ADS_SCOPES.join(","), + response_type: "code", + state, + }) + const version = config.version ?? DEFAULT_API_VERSION + return `https://www.facebook.com/${version}/dialog/oauth?${params.toString()}` + }, + exchangeCode: async ({ code, callbackUrl, credential }) => { + const config = credential as FacebookAdsConfig + const shortLivedToken = await exchangeCodeForToken( + config, + code, + callbackUrl, + ) + const longLivedToken = await exchangeLongLivedToken( + config, + shortLivedToken, + ) + return { + authType: AuthType.custom, + accessToken: longLivedToken.accessToken, + version: config.version, + expiresAt: longLivedToken.expiresIn + ? new Date(Date.now() + longLivedToken.expiresIn * 1000).toISOString() + : undefined, + } satisfies FacebookAdsAuthValue + }, + describe: (auth) => ({ + // Facebook Ads auth does not retain an ad-account identifier. + sourceId: "workspace", + displayName: "Facebook Ads", + authExpiresAt: auth.expiresAt, + }), + verify: async ({ auth }) => { + await getAdAccounts(auth.accessToken, auth.version) + return { ok: true, authExpiresAt: auth.expiresAt } + }, + isRevokedTokenError: (error) => getGraphErrorCode(error) === 190, + }, actions: { getAdAccounts: ({ ctx }) => getAdAccounts(ctx.auth.accessToken, ctx.auth.version), diff --git a/integrations/get-response/src/integration.ts b/integrations/get-response/src/integration.ts index a872af0ef9..e41de9bbaf 100644 --- a/integrations/get-response/src/integration.ts +++ b/integrations/get-response/src/integration.ts @@ -46,6 +46,47 @@ const config: IntegrationDefinition< GetResponseActions > = { name: "getResponse", + connection: { + kind: "integration", + strategy: "api_key", + multiAccount: false, + configFields: [ + { + name: "apiKey", + type: "secret", + required: true, + labelKey: "integrations.getResponse.fields.apiKey", + }, + ], + describe: () => ({ + // GetResponse auth has no stable account id; this is workspace-singleton. + sourceId: "workspace", + displayName: "GetResponse", + }), + verify: async ({ auth }) => { + try { + await getResponseRequest( + auth, + GET_RESPONSE_ACCOUNTS_PATH, + getResponseAccountsResponseSchema, + undefined, + [200], + ) + return { ok: true } + } catch (error) { + return { + ok: false, + revoked: false, + error: + error instanceof Error + ? error.message + : "Unable to verify GetResponse credentials", + } + } + }, + // TODO(connection-phase2): refine once GetResponse revoked-token error shape is confirmed. + isRevokedTokenError: () => false, + }, actions: { validateCredentials: async ({ props }) => { const auth = createGetResponseAuth(props.apiKey) diff --git a/integrations/google-calendar/__tests__/connection-oauth.test.ts b/integrations/google-calendar/__tests__/connection-oauth.test.ts new file mode 100644 index 0000000000..8a15bf33cf --- /dev/null +++ b/integrations/google-calendar/__tests__/connection-oauth.test.ts @@ -0,0 +1,139 @@ +import { beforeEach, describe, expect, test, vi } from "vitest" +import type * as CalendarsModule from "../src/apis/calendars" +import type * as ClientModule from "../src/client" + +const mocks = vi.hoisted(() => ({ + generateAuthUrl: vi.fn(), + getClient: vi.fn(), + getToken: vi.fn(), + verifyCalendarAccess: vi.fn(), +})) + +vi.mock("../src/client", async (importOriginal) => { + // Vitest loads the original at runtime so this mock can replace only getClient. + const actual = await importOriginal() + return { + ...actual, + getClient: mocks.getClient, + } +}) + +vi.mock("../src/apis/calendars", async (importOriginal) => { + // Vitest loads the original at runtime so this mock can replace only verifyCalendarAccess. + const actual = await importOriginal() + return { + ...actual, + verifyCalendarAccess: mocks.verifyCalendarAccess, + } +}) + +// The integration must load after its dependencies are mocked. +const { integration } = await import("../src/integration") + +const connection = integration.connection +if (!connection) { + throw new Error( + "Google Calendar integration must define a connection provider", + ) +} + +const credential = { + clientId: "client-1", + clientSecret: "secret-1", + redirectUrl: "https://legacy.example.test/callback", +} + +beforeEach(() => { + vi.clearAllMocks() + mocks.getClient.mockReturnValue({ + generateAuthUrl: mocks.generateAuthUrl, + getToken: mocks.getToken, + }) +}) + +describe("Google Calendar connection.authorizeUrl", () => { + test("passes state through verbatim, not JSON/base64-wrapped", () => { + mocks.generateAuthUrl.mockReturnValue( + "https://accounts.google.com/o/oauth2/v2/auth", + ) + + const url = connection.authorizeUrl?.({ + credential, + callbackUrl: "https://app.example.test/connections/callback", + state: "session-1.abc123", + }) + + expect(url).toBe("https://accounts.google.com/o/oauth2/v2/auth") + expect(mocks.getClient).toHaveBeenCalledWith({ + ...credential, + redirectUrl: "https://app.example.test/connections/callback", + }) + expect(mocks.generateAuthUrl).toHaveBeenCalledWith({ + access_type: "offline", + prompt: "consent", + scope: [ + "https://www.googleapis.com/auth/calendar.readonly", + "https://www.googleapis.com/auth/calendar.events", + ], + state: "session-1.abc123", + }) + }) +}) + +describe("Google Calendar connection.exchangeCode", () => { + test("returns the calendar-verified OAuth auth value", async () => { + mocks.getToken.mockResolvedValue({ + tokens: { + access_token: "access-token", + expiry_date: 1_700_000_000_000, + refresh_token: "refresh-token", + scope: "https://www.googleapis.com/auth/calendar.events", + }, + }) + mocks.verifyCalendarAccess.mockResolvedValue({ + providerCalendarId: "calendar@example.test", + email: "calendar@example.test", + }) + + const auth = await connection.exchangeCode?.({ + code: "auth-code", + callbackUrl: "https://app.example.test/connections/callback", + credential, + }) + + expect(mocks.getToken).toHaveBeenCalledWith("auth-code") + expect(mocks.verifyCalendarAccess).toHaveBeenCalledWith( + { + authType: "oauth2", + clientId: "client-1", + clientSecret: "secret-1", + redirectUrl: "", + tokens: { + accessToken: "access-token", + expiresAt: "2023-11-14T22:13:20.000Z", + refreshToken: "refresh-token", + }, + metadata: { + scope: "https://www.googleapis.com/auth/calendar.events", + }, + }, + "primary", + ) + expect(auth).toEqual({ + authType: "oauth2", + clientId: "client-1", + clientSecret: "secret-1", + redirectUrl: "", + tokens: { + accessToken: "access-token", + expiresAt: "2023-11-14T22:13:20.000Z", + refreshToken: "refresh-token", + }, + metadata: { + scope: "https://www.googleapis.com/auth/calendar.events", + providerCalendarId: "calendar@example.test", + email: "calendar@example.test", + }, + }) + }) +}) diff --git a/integrations/google-calendar/src/integration.ts b/integrations/google-calendar/src/integration.ts index c611618808..31851f3e2e 100644 --- a/integrations/google-calendar/src/integration.ts +++ b/integrations/google-calendar/src/integration.ts @@ -1,5 +1,6 @@ import { AuthException, + AuthType, HandleRequestType, Integration, type IntegrationDefinition, @@ -8,8 +9,13 @@ import { import { getBusyEvents } from "./apis/busy-events" import { verifyCalendarAccess } from "./apis/calendars" import { cancelEvent, createEvent } from "./apis/events" -import { generateAuthUrl, getClient, revokeToken } from "./client" -import { handleError } from "./error" +import { + GOOGLE_CALENDAR_SCOPES, + generateAuthUrl, + getClient, + revokeToken, +} from "./client" +import { getGaxiosStatus, handleError } from "./error" import { callbackHandler } from "./handlers/callback" import type { GoogleCalendarActions, @@ -23,6 +29,68 @@ const config: IntegrationDefinition< GoogleCalendarActions > = { name: "googleCalendar", + connection: { + kind: "integration", + strategy: "oauth_redirect", + multiAccount: true, + configFields: [], + authorizeUrl: ({ credential, callbackUrl, state }) => { + const config = credential as GoogleCalendarConfig + return getClient({ ...config, redirectUrl: callbackUrl }).generateAuthUrl( + { + access_type: "offline", + prompt: "consent", + scope: GOOGLE_CALENDAR_SCOPES, + state, + }, + ) + }, + exchangeCode: async ({ code, callbackUrl, credential }) => { + const config = credential as GoogleCalendarConfig + const tokens = await getClient({ + ...config, + redirectUrl: callbackUrl, + }).getToken(code) + const auth = { + authType: AuthType.oauth2, + clientId: config.clientId, + clientSecret: config.clientSecret, + redirectUrl: "", + tokens: { + accessToken: tokens.tokens.access_token || "", + expiresAt: tokens.tokens.expiry_date + ? new Date(tokens.tokens.expiry_date).toISOString() + : undefined, + refreshToken: tokens.tokens.refresh_token ?? null, + }, + metadata: { + scope: tokens.tokens.scope, + }, + } satisfies GoogleCalendarAuthValue + const calendar = await verifyCalendarAccess(auth, "primary") + + return { + ...auth, + metadata: { + ...auth.metadata, + ...calendar, + }, + } satisfies GoogleCalendarAuthValue + }, + describe: (auth) => ({ + sourceId: auth.metadata?.providerCalendarId ?? "workspace", + displayName: auth.metadata?.email ?? "Google Calendar", + authExpiresAt: auth.tokens.expiresAt, + }), + verify: async ({ auth }) => { + await verifyCalendarAccess( + auth, + auth.metadata?.providerCalendarId ?? "primary", + ) + return { ok: true, authExpiresAt: auth.tokens.expiresAt } + }, + isRevokedTokenError: (error) => getGaxiosStatus(error) === 401, + }, actions: { verifyCalendar: async ({ ctx, props }) => await verifyCalendarAccess(ctx.auth, props.calendarId), diff --git a/integrations/google-sheets/__tests__/connection-oauth.test.ts b/integrations/google-sheets/__tests__/connection-oauth.test.ts new file mode 100644 index 0000000000..aa6cb0e9d4 --- /dev/null +++ b/integrations/google-sheets/__tests__/connection-oauth.test.ts @@ -0,0 +1,99 @@ +import { beforeEach, describe, expect, test, vi } from "vitest" + +const mocks = vi.hoisted(() => ({ + generateAuthUrl: vi.fn(), + getClient: vi.fn(), + getToken: vi.fn(), +})) + +vi.mock("../src/client", () => ({ + generateAuthUrl: vi.fn(), + getClient: mocks.getClient, + getSheetsClient: vi.fn(), +})) + +// Import after mocks so the integration captures the mocked OAuth client. +const { integration } = await import("../src/integration") + +const credential = { + clientId: "client-1", + clientSecret: "secret-1", + redirectUrl: "", +} + +beforeEach(() => { + vi.clearAllMocks() + mocks.getClient.mockReturnValue({ + generateAuthUrl: mocks.generateAuthUrl, + getToken: mocks.getToken, + }) + mocks.generateAuthUrl.mockImplementation((options: { state?: string }) => { + const params = new URLSearchParams({ state: options.state ?? "" }) + return `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}` + }) + mocks.getToken.mockResolvedValue({ + tokens: { + access_token: "access-token", + expiry_date: 1_789_744_000_000, + refresh_token: "refresh-token", + scope: "https://www.googleapis.com/auth/spreadsheets", + }, + }) +}) + +describe("Google Sheets connection.authorizeUrl", () => { + test("passes state through verbatim, not JSON/base64-wrapped", () => { + const url = integration.connection.authorizeUrl?.({ + credential, + callbackUrl: + "https://app.example.test/integrations/google-sheets/callback", + state: "session-1.abc123", + }) + const parsed = new URL(url as string) + + expect(parsed.searchParams.get("state")).toBe("session-1.abc123") + expect(mocks.getClient).toHaveBeenCalledWith({ + ...credential, + redirectUrl: + "https://app.example.test/integrations/google-sheets/callback", + }) + expect(mocks.generateAuthUrl).toHaveBeenCalledWith({ + access_type: "offline", + prompt: "consent", + scope: ["https://www.googleapis.com/auth/spreadsheets"], + state: "session-1.abc123", + }) + }) +}) + +describe("Google Sheets connection.exchangeCode", () => { + test("exchanges the code and returns the complete Sheets auth", async () => { + const auth = await integration.connection.exchangeCode?.({ + code: "auth-code", + callbackUrl: + "https://app.example.test/integrations/google-sheets/callback", + credential, + }) + + expect(mocks.getClient).toHaveBeenCalledWith({ + ...credential, + redirectUrl: + "https://app.example.test/integrations/google-sheets/callback", + }) + expect(mocks.getToken).toHaveBeenCalledWith("auth-code") + expect(auth).toEqual({ + authType: "oauth2", + clientId: "client-1", + clientSecret: "secret-1", + redirectUrl: "", + tokens: { + accessToken: "access-token", + expiresAt: "2026-09-18T15:06:40.000Z", + refreshToken: "refresh-token", + }, + metadata: { + scope: "https://www.googleapis.com/auth/spreadsheets", + }, + }) + }) +}) diff --git a/integrations/google-sheets/src/integration.ts b/integrations/google-sheets/src/integration.ts index 737a7be6a2..6e9a317123 100644 --- a/integrations/google-sheets/src/integration.ts +++ b/integrations/google-sheets/src/integration.ts @@ -1,4 +1,5 @@ import { + AuthType, HandleRequestType, Integration, type IntegrationDefinition, @@ -18,6 +19,61 @@ const config: IntegrationDefinition< GoogleSheetsActions > = { name: "googleSheets", + connection: { + kind: "integration", + strategy: "oauth_redirect", + multiAccount: true, + configFields: [], + // Bypasses `generateAuthUrl`: that helper base64-JSON-encodes + // `stateParams` for the legacy cookie flow, while the Connection OAuth + // callback hub matches the raw `"{sessionId}.{nonce}"` state value. + authorizeUrl: ({ credential, callbackUrl, state }) => { + const config = credential as GoogleSheetsConfig + return getClient({ + ...config, + redirectUrl: callbackUrl, + }).generateAuthUrl({ + access_type: "offline", + prompt: "consent", + scope: ["https://www.googleapis.com/auth/spreadsheets"], + state, + }) + }, + exchangeCode: async ({ code, callbackUrl, credential }) => { + const config = credential as GoogleSheetsConfig + const tokens = await getClient({ + ...config, + redirectUrl: callbackUrl, + }).getToken(code) + + return { + authType: AuthType.oauth2, + clientId: config.clientId, + clientSecret: config.clientSecret, + redirectUrl: "", + tokens: { + accessToken: tokens.tokens.access_token || "", + expiresAt: new Date(tokens.tokens.expiry_date ?? "").toISOString(), + refreshToken: tokens.tokens.refresh_token ?? null, + }, + metadata: { + scope: tokens.tokens.scope, + }, + } satisfies GoogleSheetsAuthValue + }, + describe: (auth) => ({ + // Google Sheets auth does not retain a spreadsheet identifier. + sourceId: "workspace", + displayName: "Google Sheets", + authExpiresAt: auth.tokens.expiresAt, + }), + verify: async ({ auth }) => { + await getClient(auth).getTokenInfo(auth.tokens.accessToken) + return { ok: true, authExpiresAt: auth.tokens.expiresAt } + }, + // TODO(connection-phase2): refine once Google Sheets revoked-token error shape is confirmed. + isRevokedTokenError: () => false, + }, actions: { listSheetNames: async ({ ctx, props }): Promise => { const sheetsClient = getSheetsClient(ctx.auth) diff --git a/integrations/instagram-facebook/__tests__/connection-oauth.test.ts b/integrations/instagram-facebook/__tests__/connection-oauth.test.ts new file mode 100644 index 0000000000..48d685e676 --- /dev/null +++ b/integrations/instagram-facebook/__tests__/connection-oauth.test.ts @@ -0,0 +1,187 @@ +import { beforeEach, describe, expect, test, vi } from "vitest" + +const mocks = vi.hoisted(() => ({ + exchangeCodeForToken: vi.fn(), + getUserInstagramAccounts: vi.fn(), +})) + +vi.mock("../src/apis/auth", async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + exchangeCodeForToken: mocks.exchangeCodeForToken, + getUserInstagramAccounts: mocks.getUserInstagramAccounts, + } +}) + +const { integration } = await import("../src/integration") + +const credential = { + clientId: "client-1", + clientSecret: "secret-1", + redirectUrl: "", + version: "v23.0", + stateParams: { workspaceId: "workspace-1" }, +} + +describe("Instagram Facebook connection.authorizeUrl", () => { + test("passes state through verbatim, not JSON/base64-wrapped", () => { + const url = integration.connection.authorizeUrl?.({ + credential, + callbackUrl: + "https://app.example.test/integrations/instagram-facebook/callback", + state: "session-1.abc123", + }) + const parsed = new URL(url as string) + + expect(parsed.searchParams.get("state")).toBe("session-1.abc123") + expect(parsed.searchParams.get("client_id")).toBe("client-1") + expect(parsed.searchParams.get("redirect_uri")).toBe( + "https://app.example.test/integrations/instagram-facebook/callback", + ) + expect(parsed.hostname).toBe("www.facebook.com") + }) +}) + +describe("Instagram Facebook connection.exchangeCode", () => { + beforeEach(() => { + vi.clearAllMocks() + mocks.exchangeCodeForToken.mockResolvedValue("user-access-token") + }) + + test("returns a user-level oauth2 AuthValue with no account metadata yet", async () => { + const auth = await integration.connection.exchangeCode?.({ + code: "auth-code", + callbackUrl: "https://app.example.test/callback", + credential, + }) + + expect(mocks.exchangeCodeForToken).toHaveBeenCalledWith( + credential, + "auth-code", + "https://app.example.test/callback", + ) + expect(auth).toEqual({ + authType: "oauth2", + clientId: "client-1", + clientSecret: "secret-1", + redirectUrl: "", + version: "v23.0", + tokens: { accessToken: "user-access-token" }, + }) + }) +}) + +describe("Instagram Facebook connection.listCandidates", () => { + const userAuth = { + authType: "oauth2" as const, + clientId: "client-1", + clientSecret: "secret-1", + redirectUrl: "", + version: "v23.0", + tokens: { accessToken: "user-access-token" }, + } + + beforeEach(() => { + vi.clearAllMocks() + }) + + test("maps every Instagram account to its own page-scoped auth and metadata", async () => { + mocks.getUserInstagramAccounts.mockResolvedValue([ + { + id: "ig-1", + name: "Instagram One", + username: "instagram-one", + pageId: "page-1", + pageAccessToken: "page-1-token", + }, + { + id: "ig-2", + name: "Instagram Two", + username: "instagram-two", + profile_picture_url: "https://example.test/avatar.png", + pageId: "page-2", + pageAccessToken: "page-2-token", + }, + ]) + + const candidates = await integration.connection.listCandidates?.({ + auth: userAuth, + }) + + expect(mocks.getUserInstagramAccounts).toHaveBeenCalledWith( + "user-access-token", + "v23.0", + ) + expect(candidates).toEqual([ + { + sourceId: "ig-1", + displayName: "Instagram One", + auth: { + authType: "oauth2", + clientId: "client-1", + clientSecret: "secret-1", + redirectUrl: "", + version: "v23.0", + tokens: { accessToken: "page-1-token" }, + metadata: { + igId: "ig-1", + igName: "Instagram One", + pageId: "page-1", + version: "v23.0", + username: "instagram-one", + }, + }, + }, + { + sourceId: "ig-2", + displayName: "Instagram Two", + auth: { + authType: "oauth2", + clientId: "client-1", + clientSecret: "secret-1", + redirectUrl: "", + version: "v23.0", + tokens: { accessToken: "page-2-token" }, + metadata: { + igId: "ig-2", + igName: "Instagram Two", + pageId: "page-2", + version: "v23.0", + username: "instagram-two", + }, + }, + }, + ]) + }) + + test("returns no candidates for a non-oauth2 auth value", async () => { + const candidates = await integration.connection.listCandidates?.({ + auth: { authType: "none" }, + }) + + expect(candidates).toEqual([]) + expect(mocks.getUserInstagramAccounts).not.toHaveBeenCalled() + }) +}) + +describe("Instagram Facebook connection.candidateToConfig", () => { + test("returns page and username fields required by the satellite table", () => { + const config = integration.connection.candidateToConfig?.({ + authType: "oauth2", + clientId: "client-1", + clientSecret: "secret-1", + redirectUrl: "", + tokens: { accessToken: "page-access-token" }, + metadata: { + igId: "ig-1", + igName: "Instagram One", + pageId: "page-1", + version: "v23.0", + username: "instagram-one", + }, + }) + + expect(config).toEqual({ pageId: "page-1", username: "instagram-one" }) + }) +}) diff --git a/integrations/instagram-facebook/src/integration.ts b/integrations/instagram-facebook/src/integration.ts index bef5afc1dc..836940cf93 100644 --- a/integrations/instagram-facebook/src/integration.ts +++ b/integrations/instagram-facebook/src/integration.ts @@ -1,13 +1,22 @@ import { + AuthType, HandleRequestType, Integration, type IntegrationDefinition, } from "@chatbotx.io/sdk" +import { + debugToken, + exchangeCodeForToken, + getUserInstagramAccounts, + toAppAccessToken, +} from "./apis/auth" import { exchangeLongLivedToken, + subscribePageToInstagramWebhook, unsubscribePageFromInstagramWebhook, } from "./apis/page" import { getPostDetails } from "./apis/post" +import { DEFAULT_API_VERSION } from "./constants" import { InstagramAPIException } from "./exception" import { botHandlers } from "./handlers/bot" import { commentHandlers } from "./handlers/comment" @@ -15,18 +24,131 @@ import { contactHandlers } from "./handlers/contact" import { conversationHandlers } from "./handlers/conversation" import { messageHandlers } from "./handlers/message" import { webhookHandler } from "./handlers/webhook" +import { isRevokedTokenError } from "./lib/error-mapper" import type { InstagramActions, InstagramAuthValue, InstagramConfig, } from "./schemas" +const INSTAGRAM_OAUTH_SCOPES = [ + "instagram_basic", + "instagram_manage_comments", + "instagram_manage_engagement", + "instagram_manage_messages", + "instagram_manage_events", + "pages_manage_metadata", + "pages_show_list", + "pages_messaging", + "pages_read_engagement", + "business_management", +] + const config: IntegrationDefinition< InstagramConfig, InstagramAuthValue, InstagramActions > = { name: "instagramFacebook", + connection: { + kind: "channel", + strategy: "oauth_redirect", + multiAccount: true, + configFields: [], + authorizeUrl: ({ credential, callbackUrl, state }) => { + const config = credential as InstagramConfig + const params = new URLSearchParams({ + client_id: config.clientId, + redirect_uri: callbackUrl, + response_type: "code", + scope: INSTAGRAM_OAUTH_SCOPES.join(","), + state, + }) + return `https://www.facebook.com/${config.version}/dialog/oauth?${params.toString()}` + }, + exchangeCode: async ({ code, callbackUrl, credential }) => { + const config = credential as InstagramConfig + const accessToken = await exchangeCodeForToken(config, code, callbackUrl) + return { + authType: AuthType.oauth2, + clientId: config.clientId, + clientSecret: config.clientSecret, + redirectUrl: "", + version: config.version, + tokens: { accessToken }, + } + }, + listCandidates: async ({ auth }) => { + if (auth.authType !== AuthType.oauth2) { + return [] + } + + const version = auth.version ?? DEFAULT_API_VERSION + const accounts = await getUserInstagramAccounts( + auth.tokens.accessToken, + version, + ) + return accounts.map((account) => ({ + sourceId: account.id, + displayName: account.name, + auth: { + authType: AuthType.oauth2, + clientId: auth.clientId, + clientSecret: auth.clientSecret, + redirectUrl: "", + version, + tokens: { accessToken: account.pageAccessToken }, + metadata: { + igId: account.id, + igName: account.name, + pageId: account.pageId, + version, + username: account.username, + }, + } satisfies InstagramAuthValue, + })) + }, + candidateToConfig: (auth) => ({ + pageId: auth.metadata.pageId, + username: auth.metadata.username, + }), + describe: (auth) => ({ + sourceId: auth.metadata.igId, + displayName: auth.metadata.igName, + }), + verify: async ({ auth }) => { + const token = await debugToken({ + inputToken: auth.tokens.accessToken, + appAccessToken: toAppAccessToken(auth), + version: auth.metadata.version, + }) + + if (token.is_valid !== true) { + return { + ok: false, + revoked: true, + error: "Instagram access token is invalid", + } + } + + return { ok: true, authExpiresAt: auth.tokens.expiresAt } + }, + isRevokedTokenError, + webhook: { + subscribe: ({ auth }) => + subscribePageToInstagramWebhook({ + pageId: auth.metadata.pageId, + accessToken: auth.tokens.accessToken, + version: auth.metadata.version, + }), + unsubscribe: ({ auth }) => + unsubscribePageFromInstagramWebhook({ + pageId: auth.metadata.pageId, + appAccessToken: toAppAccessToken(auth), + version: auth.metadata.version, + }), + }, + }, channels: { channel: { message: messageHandlers, diff --git a/integrations/instagram/src/integration.ts b/integrations/instagram/src/integration.ts index 35e9212d78..53a7542deb 100644 --- a/integrations/instagram/src/integration.ts +++ b/integrations/instagram/src/integration.ts @@ -1,13 +1,17 @@ import { + AuthType, HandleRequestType, Integration, type IntegrationDefinition, } from "@chatbotx.io/sdk" +import { exchangeCodeForToken, getInstagramAccount } from "./apis/auth" import { refreshLongLivedToken, + subscribePageToInstagramWebhook, unsubscribePageFromInstagramWebhook, } from "./apis/page" import { getPostDetails } from "./apis/post" +import { INSTAGRAM_BUSINESS_SCOPES } from "./constants" import { InstagramAPIException } from "./exception" import { botHandlers } from "./handlers/bot" import { commentHandlers } from "./handlers/comment" @@ -15,6 +19,7 @@ import { contactHandlers } from "./handlers/contact" import { conversationHandlers } from "./handlers/conversation" import { messageHandlers } from "./handlers/message" import { webhookHandler } from "./handlers/webhook" +import { isRevokedTokenError } from "./lib/error-mapper" import type { InstagramActions, InstagramAuthValue, @@ -27,6 +32,98 @@ const config: IntegrationDefinition< InstagramActions > = { name: "instagram", + connection: { + kind: "channel", + strategy: "oauth_redirect", + multiAccount: true, + configFields: [], + // Bypasses `generateAuthUrl` deliberately: it base64-JSON-encodes + // `stateParams` into the `state` query param for the legacy per-request + // cookie flow, but the Connection domain's OAuth callback hub matches + // `state` against a raw `"{sessionId}.{nonce}"` string. + authorizeUrl: ({ credential, callbackUrl, state }) => { + const config = credential as InstagramConfig + const params = new URLSearchParams({ + client_id: config.clientId, + redirect_uri: callbackUrl, + response_type: "code", + state, + scope: INSTAGRAM_BUSINESS_SCOPES.join(","), + }) + return `https://www.instagram.com/oauth/authorize?${params.toString()}` + }, + // Instagram Business Login authorizes exactly one account per grant — + // `exchangeCodeForToken` already returns a long-lived, account-scoped + // token (unlike Messenger's user-level token), so this resolves the + // full `InstagramAuthValue` directly and no `listCandidates` is needed; + // `completeAuthorization`'s single-candidate fallback + // (`[{...describe(auth), auth}]`) covers it. `username` has no default + // on `IntegrationInstagram` and isn't part of `describe()`'s + // `{sourceId,displayName}`, so it rides along in `metadata` for + // `candidateToConfig` to surface. + exchangeCode: async ({ code, callbackUrl, credential }) => { + const config = credential as InstagramConfig + const { accessToken, userId } = await exchangeCodeForToken( + config, + code, + callbackUrl, + ) + const account = await getInstagramAccount(accessToken) + if (!account) { + throw new Error( + "Instagram account is not a supported Business/Creator account.", + ) + } + return { + authType: AuthType.oauth2, + clientId: config.clientId, + clientSecret: config.clientSecret, + redirectUrl: "", + version: config.version, + tokens: { accessToken }, + metadata: { + igId: userId, + igName: account.name, + pageId: account.id, + version: config.version, + username: account.username, + }, + } satisfies InstagramAuthValue + }, + candidateToConfig: (auth) => ({ username: auth.metadata.username }), + describe: (auth) => ({ + sourceId: auth.metadata.igId, + displayName: auth.metadata.igName, + }), + verify: async ({ auth }) => { + const account = await getInstagramAccount(auth.tokens.accessToken) + + if (!account || account.id !== auth.metadata.igId) { + return { + ok: false, + revoked: false, + error: "Instagram account could not be verified", + } + } + + return { ok: true, authExpiresAt: auth.tokens.expiresAt } + }, + isRevokedTokenError, + webhook: { + subscribe: ({ auth }) => + subscribePageToInstagramWebhook({ + igId: auth.metadata.igId, + accessToken: auth.tokens.accessToken, + version: auth.metadata.version, + }), + unsubscribe: ({ auth }) => + unsubscribePageFromInstagramWebhook({ + igId: auth.metadata.igId, + accessToken: auth.tokens.accessToken, + version: auth.metadata.version, + }), + }, + }, channels: { channel: { message: messageHandlers, diff --git a/integrations/klaviyo/src/integration.ts b/integrations/klaviyo/src/integration.ts index 94850d7a02..655f4b206c 100644 --- a/integrations/klaviyo/src/integration.ts +++ b/integrations/klaviyo/src/integration.ts @@ -40,6 +40,54 @@ const config: IntegrationDefinition< KlaviyoActions > = { name: "klaviyo", + connection: { + kind: "integration", + strategy: "api_key", + multiAccount: false, + configFields: [ + { + name: "apiKey", + type: "secret", + required: true, + labelKey: "integrations.klaviyo.fields.apiKey", + }, + ], + describe: () => ({ + // Klaviyo auth contains no stable account identifier; this is workspace-scoped. + sourceId: "workspace", + displayName: "Klaviyo", + }), + verify: async ({ auth }) => { + try { + await klaviyoRequest( + auth, + KLAVIYO_LISTS_PATH, + klaviyoListsResponseSchema, + { searchParams: pageSearchParams({ size: 1 }) }, + [200], + ) + return { ok: true } + } catch (error) { + return { + ok: false, + revoked: + typeof error === "object" && + error !== null && + "statusCode" in error && + (error.statusCode === 401 || error.statusCode === 403), + error: + error instanceof Error + ? error.message + : "Klaviyo credential verification failed", + } + } + }, + isRevokedTokenError: (error) => + typeof error === "object" && + error !== null && + "statusCode" in error && + (error.statusCode === 401 || error.statusCode === 403), + }, actions: { validateCredentials: async ({ props }) => { const auth = createKlaviyoAuth(props.apiKey) diff --git a/integrations/mailchimp/src/integration.ts b/integrations/mailchimp/src/integration.ts index b1068b9477..bd56c436a8 100644 --- a/integrations/mailchimp/src/integration.ts +++ b/integrations/mailchimp/src/integration.ts @@ -30,6 +30,52 @@ const config: IntegrationDefinition< MailchimpActions > = { name: "mailchimp", + connection: { + kind: "integration", + strategy: "api_key", + multiAccount: false, + configFields: [ + { + name: "apiKey", + type: "secret", + required: true, + labelKey: "integrations.mailchimp.fields.apiKey", + }, + ], + describe: () => ({ + // Mailchimp auth contains no stable account identifier; this is workspace-scoped. + sourceId: "workspace", + displayName: "Mailchimp", + }), + verify: async ({ auth }) => { + try { + await mailchimpRequest( + auth, + MAILCHIMP_PING_ENDPOINT, + mailchimpPingResponseSchema, + ) + return { ok: true } + } catch (error) { + return { + ok: false, + revoked: + typeof error === "object" && + error !== null && + "statusCode" in error && + (error.statusCode === 401 || error.statusCode === 403), + error: + error instanceof Error + ? error.message + : "Mailchimp credential verification failed", + } + } + }, + isRevokedTokenError: (error) => + typeof error === "object" && + error !== null && + "statusCode" in error && + (error.statusCode === 401 || error.statusCode === 403), + }, actions: { validateApiKey: async ({ props }) => { const auth = createMailchimpAuth(props.apiKey) diff --git a/integrations/mailer-lite/src/integration.ts b/integrations/mailer-lite/src/integration.ts index 6614c3e94c..6ed10266f6 100644 --- a/integrations/mailer-lite/src/integration.ts +++ b/integrations/mailer-lite/src/integration.ts @@ -44,6 +44,54 @@ const config: IntegrationDefinition< MailerLiteActions > = { name: "mailerLite", + connection: { + kind: "integration", + strategy: "api_key", + multiAccount: false, + configFields: [ + { + name: "apiKey", + type: "secret", + required: true, + labelKey: "integrations.mailerLite.fields.apiKey", + }, + ], + describe: () => ({ + // MailerLite auth contains no stable account identifier; this is workspace-scoped. + sourceId: "workspace", + displayName: "MailerLite", + }), + verify: async ({ auth }) => { + try { + await mailerLiteRequest( + auth, + MAILER_LITE_GROUPS_PATH, + mailerLiteGroupsResponseSchema, + { searchParams: pageSearchParams({ page: 1, limit: 1 }) }, + [200], + ) + return { ok: true } + } catch (error) { + return { + ok: false, + revoked: + typeof error === "object" && + error !== null && + "statusCode" in error && + (error.statusCode === 401 || error.statusCode === 403), + error: + error instanceof Error + ? error.message + : "MailerLite credential verification failed", + } + } + }, + isRevokedTokenError: (error) => + typeof error === "object" && + error !== null && + "statusCode" in error && + (error.statusCode === 401 || error.statusCode === 403), + }, actions: { validateCredentials: async ({ props }) => { const auth = createMailerLiteAuth(props.apiKey) diff --git a/integrations/messenger/__tests__/connection-oauth.test.ts b/integrations/messenger/__tests__/connection-oauth.test.ts new file mode 100644 index 0000000000..cab1afa154 --- /dev/null +++ b/integrations/messenger/__tests__/connection-oauth.test.ts @@ -0,0 +1,163 @@ +import { beforeEach, describe, expect, test, vi } from "vitest" + +const mocks = vi.hoisted(() => ({ + exchangeCodeForToken: vi.fn(), + exchangeLongLivedToken: vi.fn(), + getUserPages: vi.fn(), + loggerInfo: vi.fn(), +})) + +vi.mock("../src/apis/auth", async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + exchangeCodeForToken: mocks.exchangeCodeForToken, + getUserPages: mocks.getUserPages, + } +}) + +vi.mock("../src/apis/page", async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + exchangeLongLivedToken: mocks.exchangeLongLivedToken, + } +}) + +vi.mock("../src/lib/logger", () => ({ + logger: { + warn: vi.fn(), + error: vi.fn(), + info: mocks.loggerInfo, + debug: vi.fn(), + }, +})) + +const { integration } = await import("../src/integration") + +const credential = { + clientId: "client-1", + clientSecret: "secret-1", + redirectUrl: "", + version: "v23.0", + stateParams: { workspaceId: "workspace-1" }, +} + +describe("Messenger connection.authorizeUrl", () => { + test("passes state through verbatim, not JSON/base64-wrapped", () => { + const url = integration.connection.authorizeUrl?.({ + credential, + callbackUrl: "https://app.example.test/integrations/messenger/callback", + state: "session-1.abc123", + }) + const parsed = new URL(url as string) + expect(parsed.searchParams.get("state")).toBe("session-1.abc123") + expect(parsed.searchParams.get("client_id")).toBe("client-1") + expect(parsed.searchParams.get("redirect_uri")).toBe( + "https://app.example.test/integrations/messenger/callback", + ) + expect(parsed.hostname).toBe("www.facebook.com") + }) +}) + +describe("Messenger connection.exchangeCode", () => { + beforeEach(() => { + vi.clearAllMocks() + mocks.exchangeCodeForToken.mockResolvedValue("short-lived-token") + mocks.exchangeLongLivedToken.mockResolvedValue("long-lived-token") + }) + + test("returns a user-level oauth2 AuthValue with no page metadata yet", async () => { + const auth = await integration.connection.exchangeCode?.({ + code: "auth-code", + callbackUrl: "https://app.example.test/callback", + credential, + }) + expect(auth).toEqual({ + authType: "oauth2", + clientId: "client-1", + clientSecret: "secret-1", + redirectUrl: "", + version: "v23.0", + tokens: { accessToken: "long-lived-token" }, + }) + }) + + test("falls back to the short-lived token when the long-lived exchange fails", async () => { + mocks.exchangeLongLivedToken.mockRejectedValueOnce(new Error("boom")) + const auth = await integration.connection.exchangeCode?.({ + code: "auth-code", + callbackUrl: "https://app.example.test/callback", + credential, + }) + expect( + (auth as { tokens: { accessToken: string } }).tokens.accessToken, + ).toBe("short-lived-token") + expect(mocks.loggerInfo).toHaveBeenCalled() + }) +}) + +describe("Messenger connection.listCandidates", () => { + const userAuth = { + authType: "oauth2" as const, + clientId: "client-1", + clientSecret: "secret-1", + redirectUrl: "", + version: "v23.0", + tokens: { accessToken: "user-token" }, + } + + test("returns one candidate per connectable page, carrying that page's own access token", async () => { + mocks.getUserPages.mockResolvedValue({ + pages: [ + { + id: "page-1", + name: "Page One", + isConnectable: true, + access_token: "page-1-token", + }, + { + id: "page-2", + name: "Page Two", + isConnectable: false, + access_token: "page-2-token", + }, + { id: "page-3", name: "Page Three", isConnectable: true }, + ], + bmLookupFailed: false, + }) + + const candidates = await integration.connection.listCandidates?.({ + auth: userAuth, + }) + + expect(mocks.getUserPages).toHaveBeenCalledWith("user-token", "v23.0") + expect(candidates).toEqual([ + { + sourceId: "page-1", + displayName: "Page One", + auth: { + authType: "oauth2", + clientId: "client-1", + clientSecret: "secret-1", + redirectUrl: "", + version: "v23.0", + tokens: { accessToken: "page-1-token" }, + metadata: { + pageId: "page-1", + pageName: "Page One", + version: "v23.0", + }, + }, + }, + ]) + }) + + test("returns no candidates for a non-oauth2 auth value", async () => { + const candidates = await integration.connection.listCandidates?.({ + auth: { authType: "none" }, + }) + expect(candidates).toEqual([]) + expect(mocks.getUserPages).not.toHaveBeenCalled() + }) +}) diff --git a/integrations/messenger/src/integration.ts b/integrations/messenger/src/integration.ts index 58af1f53bd..fe1cd2489a 100644 --- a/integrations/messenger/src/integration.ts +++ b/integrations/messenger/src/integration.ts @@ -1,8 +1,16 @@ import { + AuthType, HandleRequestType, Integration, type IntegrationDefinition, } from "@chatbotx.io/sdk" +import { + debugToken, + exchangeCodeForToken, + getUserPages, + MESSENGER_SCOPES, + toAppAccessToken, +} from "./apis/auth" import { getCommentAttachment, getCommentAttachmentType, @@ -16,11 +24,13 @@ import { import { deleteMessengerProfileFields, exchangeLongLivedToken, + subscribePageToAppWebhook, syncPersonas, unsubscribePageFromAppWebhook, } from "./apis/page" import { getPostDetails } from "./apis/post" import { getUserInboxLink } from "./apis/user-inbox-link" +import { DEFAULT_API_VERSION } from "./constants" import { MessengerAPIException } from "./exception" import { botHandlers } from "./handlers/bot" import { commentHandlers } from "./handlers/comment" @@ -28,6 +38,7 @@ import { contactHandlers } from "./handlers/contact" import { conversationHandlers } from "./handlers/conversation" import { messageHandlers } from "./handlers/message" import { webhookHandler } from "./handlers/webhook" +import { isRevokedTokenError } from "./lib/error-mapper" import { logger } from "./lib/logger" import type { MessengerActions, @@ -41,6 +52,123 @@ const config: IntegrationDefinition< MessengerActions > = { name: "messenger", + connection: { + kind: "channel", + strategy: "oauth_redirect", + multiAccount: true, + configFields: [], + // Bypasses `generateAuthUrl` deliberately: that helper base64-JSON- + // encodes `stateParams` into the `state` query param for the legacy + // per-request cookie flow, but the Connection domain's OAuth callback + // hub matches `state` against a raw `"{sessionId}.{nonce}"` string — + // wrapping it in JSON here would make every session-based Messenger + // connect silently fall through to the legacy branch. + authorizeUrl: ({ credential, callbackUrl, state }) => { + const config = credential as MessengerConfig + const params = new URLSearchParams({ + auth_type: "rerequest", + client_id: config.clientId, + redirect_uri: callbackUrl, + scope: MESSENGER_SCOPES.join(","), + response_type: "code", + state, + }) + return `https://www.facebook.com/${config.version}/dialog/oauth?${params.toString()}` + }, + // Returns a *user*-level `AuthValue` (SDK-generalized, not `MessengerAuthValue` + // — the exchanged token isn't tied to a page yet, so it can't carry + // `metadata.pageId`). Mirrors the OAuth callback hub's existing + // short-lived -> long-lived exchange, falling back to the short-lived + // token on a failed long-lived exchange rather than failing the whole + // connect (`apps/builder/src/app/integrations/[...integration]/callback.ts`). + exchangeCode: async ({ code, callbackUrl, credential }) => { + const config = credential as MessengerConfig + const shortLivedToken = await exchangeCodeForToken( + config, + code, + callbackUrl, + ) + const longLivedToken = await exchangeLongLivedToken( + config, + shortLivedToken, + ).catch((error) => { + logger.info( + { err: error }, + "Messenger long-lived token exchange failed, using short-lived token", + ) + return shortLivedToken + }) + return { + authType: AuthType.oauth2, + clientId: config.clientId, + clientSecret: config.clientSecret, + redirectUrl: "", + version: config.version, + tokens: { accessToken: longLivedToken }, + } + }, + // One Graph call, no cache — provider lists already carry each page's + // own access token (`getUserPages`), so no per-candidate follow-up call + // is needed to build its final `MessengerAuthValue`. + listCandidates: async ({ auth }) => { + if (auth.authType !== "oauth2") { + return [] + } + const version = auth.version ?? DEFAULT_API_VERSION + const { pages } = await getUserPages(auth.tokens.accessToken, version) + return pages + .filter((page) => page.isConnectable && page.access_token) + .map((page) => ({ + sourceId: page.id, + displayName: page.name, + auth: { + authType: AuthType.oauth2, + clientId: auth.clientId, + clientSecret: auth.clientSecret, + redirectUrl: "", + version, + tokens: { accessToken: page.access_token as string }, + metadata: { pageId: page.id, pageName: page.name, version }, + } satisfies MessengerAuthValue, + })) + }, + describe: (auth) => ({ + sourceId: auth.metadata.pageId, + displayName: auth.metadata.pageName, + }), + verify: async ({ auth }) => { + const token = await debugToken({ + inputToken: auth.tokens.accessToken, + appAccessToken: toAppAccessToken(auth), + version: auth.metadata.version, + }) + + if (token.is_valid !== true) { + return { + ok: false, + revoked: true, + error: "Messenger access token is invalid", + } + } + + return { ok: true, authExpiresAt: auth.tokens.expiresAt } + }, + isRevokedTokenError, + webhook: { + subscribe: ({ auth }) => + subscribePageToAppWebhook({ + pageId: auth.metadata.pageId, + accessToken: auth.tokens.accessToken, + version: auth.metadata.version, + }), + unsubscribe: ({ auth }) => + unsubscribePageFromAppWebhook({ + pageId: auth.metadata.pageId, + appAccessToken: toAppAccessToken(auth), + version: auth.metadata.version, + }), + }, + }, channels: { channel: { message: messageHandlers, diff --git a/integrations/moosend/src/integration.ts b/integrations/moosend/src/integration.ts index 2bb6895deb..9847120040 100644 --- a/integrations/moosend/src/integration.ts +++ b/integrations/moosend/src/integration.ts @@ -22,6 +22,52 @@ const config: IntegrationDefinition< MoosendActions > = { name: "moosend", + connection: { + kind: "integration", + strategy: "api_key", + multiAccount: false, + configFields: [ + { + name: "apiKey", + type: "secret", + required: true, + labelKey: "integrations.moosend.fields.apiKey", + }, + ], + describe: () => ({ + // Moosend auth contains no stable account identifier; this is workspace-scoped. + sourceId: "workspace", + displayName: "Moosend", + }), + verify: async ({ auth }) => { + try { + await moosendRequest( + auth, + moosendListsPagePath(1, 1), + moosendMailingListsResponseSchema, + ) + return { ok: true } + } catch (error) { + return { + ok: false, + revoked: + typeof error === "object" && + error !== null && + "kind" in error && + error.kind === "invalid_credentials", + error: + error instanceof Error + ? error.message + : "Moosend credential verification failed", + } + } + }, + isRevokedTokenError: (error) => + typeof error === "object" && + error !== null && + "kind" in error && + error.kind === "invalid_credentials", + }, actions: { validateCredentials: async ({ props }) => { const auth = createMoosendAuth(props.apiKey) diff --git a/integrations/sendgrid/src/integration.ts b/integrations/sendgrid/src/integration.ts index a3e682a4c1..4df8f8a232 100644 --- a/integrations/sendgrid/src/integration.ts +++ b/integrations/sendgrid/src/integration.ts @@ -43,6 +43,52 @@ const config: IntegrationDefinition< SendGridActions > = { name: "sendGrid", + connection: { + kind: "integration", + strategy: "api_key", + multiAccount: false, + configFields: [ + { + name: "apiKey", + type: "secret", + required: true, + labelKey: "integrations.sendGrid.fields.apiKey", + }, + ], + describe: () => ({ + // SendGrid auth contains no stable account identifier; this is workspace-scoped. + sourceId: "workspace", + displayName: "SendGrid", + }), + verify: async ({ auth }) => { + try { + await sendGridRequest( + auth, + SENDGRID_SCOPES_PATH, + sendGridScopesResponseSchema, + ) + return { ok: true } + } catch (error) { + return { + ok: false, + revoked: + typeof error === "object" && + error !== null && + "statusCode" in error && + (error.statusCode === 401 || error.statusCode === 403), + error: + error instanceof Error + ? error.message + : "SendGrid credential verification failed", + } + } + }, + isRevokedTokenError: (error) => + typeof error === "object" && + error !== null && + "statusCode" in error && + (error.statusCode === 401 || error.statusCode === 403), + }, actions: { validateCredentials: async ({ props }) => { const auth = createSendGridAuth(props.apiKey) diff --git a/integrations/smtp/src/integration.ts b/integrations/smtp/src/integration.ts index 3b2140cff4..9d566aff67 100644 --- a/integrations/smtp/src/integration.ts +++ b/integrations/smtp/src/integration.ts @@ -17,6 +17,63 @@ const config: IntegrationDefinition = { actions: { sendMail, }, + connection: { + kind: "channel", + strategy: "self_serve", + multiAccount: true, + configFields: [ + { + name: "provider", + type: "enum", + required: true, + labelKey: "integrations.smtp.fields.provider", + enumValues: [ + "google", + "outlook", + "yahoo", + "sendgrid", + "mailgun", + "amazon_ses", + "zoho", + "postmark", + "brevo", + "other", + ], + }, + { + name: "host", + type: "string", + required: true, + labelKey: "integrations.smtp.fields.host", + }, + { + name: "port", + type: "number", + required: true, + labelKey: "integrations.smtp.fields.port", + }, + { + name: "username", + type: "string", + required: true, + labelKey: "integrations.smtp.fields.username", + }, + { + name: "password", + type: "secret", + required: true, + labelKey: "integrations.smtp.fields.password", + }, + ], + describe: (auth) => ({ + // SMTP credentials do not carry a stable external account ID. + sourceId: "workspace", + displayName: `SMTP (${auth.provider})`, + }), + verify: async () => ({ ok: true }), + // SMTP credentials have no token-revocation concept. + isRevokedTokenError: () => false, + }, handleRequest(_props: HandleRequestProps) { throw new Error("Method is not implemented.") }, diff --git a/integrations/telegram/src/integration.ts b/integrations/telegram/src/integration.ts index ae1821e230..363841463e 100644 --- a/integrations/telegram/src/integration.ts +++ b/integrations/telegram/src/integration.ts @@ -3,12 +3,13 @@ import { Integration, type IntegrationDefinition, } from "@chatbotx.io/sdk" -import { connect, deleteWebhook, registerWebhook } from "./apis/bot" +import { connect, deleteWebhook, getMe, registerWebhook } from "./apis/bot" import { TelegramAPIException } from "./exception" import { contactHandlers } from "./handlers/contact" import { conversationHandlers } from "./handlers/conversation" import { messageHandlers } from "./handlers/message" import { webhookHandler } from "./handlers/webhook" +import { isRevokedTokenError } from "./lib/error-mapper" import type { TelegramActions, TelegramAuthValue, @@ -40,6 +41,29 @@ const config: IntegrationDefinition< registerWebhook: async ({ botToken, webhookUrl }) => registerWebhook({ botToken, webhookUrl }), }, + connection: { + kind: "channel", + strategy: "token", + multiAccount: true, + configFields: [ + { + name: "secretText", + type: "secret", + required: true, + labelKey: "integrations.telegram.fields.secretText", + }, + ], + describe: () => ({ + // TelegramAuthValue stores only the bot token; the bot ID lives outside auth. + sourceId: "workspace", + displayName: "Telegram bot", + }), + verify: async ({ auth }) => { + await getMe(auth) + return { ok: true } + }, + isRevokedTokenError, + }, handleRequest: async (props) => { const segments = new URL(props.req.url).pathname.split("/") const action = segments.pop() diff --git a/integrations/tiktok/__tests__/connection-oauth.test.ts b/integrations/tiktok/__tests__/connection-oauth.test.ts new file mode 100644 index 0000000000..df82692245 --- /dev/null +++ b/integrations/tiktok/__tests__/connection-oauth.test.ts @@ -0,0 +1,113 @@ +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest" + +const mocks = vi.hoisted(() => ({ + exchangeCodeForToken: vi.fn(), + getUserInfo: vi.fn(), +})) + +vi.mock("../src/apis/auth", async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + exchangeCodeForToken: mocks.exchangeCodeForToken, + } +}) + +vi.mock("../src/apis/user", async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + getUserInfo: mocks.getUserInfo, + } +}) + +const { integration } = await import("../src/integration") + +const credential = { + clientId: "client-1", + clientSecret: "secret-1", + redirectUrl: "", +} + +describe("TikTok connection.authorizeUrl", () => { + test("passes state through verbatim, not JSON/base64-wrapped", () => { + const url = integration.connection.authorizeUrl?.({ + credential, + callbackUrl: "https://app.example.test/integrations/tiktok/callback", + state: "session-1.abc123", + }) + const parsed = new URL(url as string) + + expect(parsed.searchParams.get("state")).toBe("session-1.abc123") + expect(parsed.searchParams.get("client_key")).toBe("client-1") + expect(parsed.searchParams.get("redirect_uri")).toBe( + "https://app.example.test/integrations/tiktok/callback", + ) + expect(parsed.searchParams.get("disable_auto_auth")).toBe("1") + expect(parsed.searchParams.get("scope")).toBe( + "user.info.basic,user.info.username,user.info.profile,user.info.stats,user.account.type,message.list.read,message.list.send,message.list.manage", + ) + }) +}) + +describe("TikTok connection.exchangeCode", () => { + beforeEach(() => { + vi.useFakeTimers() + vi.setSystemTime(new Date("2026-09-15T12:00:00.000Z")) + vi.clearAllMocks() + mocks.exchangeCodeForToken.mockResolvedValue({ + access_token: "access-token", + refresh_token: "refresh-token", + expires_in: 3600, + refresh_expires_in: 7200, + open_id: "open-id-1", + }) + mocks.getUserInfo.mockResolvedValue({ + open_id: "open-id-1", + username: "tiktok-user", + display_name: "TikTok User", + avatar_url: "https://example.test/avatar.png", + }) + }) + + afterEach(() => { + vi.useRealTimers() + }) + + test("exchanges the code and returns the complete account auth", async () => { + const auth = await integration.connection.exchangeCode?.({ + code: "auth-code", + callbackUrl: "https://app.example.test/callback", + credential, + }) + + expect(mocks.exchangeCodeForToken).toHaveBeenCalledWith( + { + clientId: "client-1", + clientSecret: "secret-1", + redirectUrl: "https://app.example.test/callback", + }, + "auth-code", + ) + expect(mocks.getUserInfo).toHaveBeenCalledWith({ + accessToken: "access-token", + }) + expect(auth).toEqual({ + authType: "oauth2", + clientId: "client-1", + clientSecret: "secret-1", + redirectUrl: "", + tokens: { + accessToken: "access-token", + refreshToken: "refresh-token", + expiresAt: "2026-09-15T13:00:00.000Z", + refreshTokenExpiresAt: "2026-09-15T14:00:00.000Z", + }, + metadata: { + openId: "open-id-1", + username: "tiktok-user", + displayName: "TikTok User", + }, + }) + }) +}) diff --git a/integrations/tiktok/src/integration.ts b/integrations/tiktok/src/integration.ts index 3766324c99..f2e6ff6627 100644 --- a/integrations/tiktok/src/integration.ts +++ b/integrations/tiktok/src/integration.ts @@ -1,19 +1,33 @@ import { AuthException, + AuthType, HandleRequestType, Integration, type IntegrationDefinition, } from "@chatbotx.io/sdk" -import { refreshAccessToken } from "./apis/auth" +import { exchangeCodeForToken, refreshAccessToken } from "./apis/auth" +import { getUserInfo } from "./apis/user" import { TiktokAPIException } from "./exception" import { callbackHandler } from "./handlers/callback" import { contactHandlers } from "./handlers/contact" import { conversationHandlers } from "./handlers/conversation" import { messageHandlers } from "./handlers/message" import { webhookHandler } from "./handlers/webhook" +import { isRevokedTokenError } from "./lib/error-mapper" import { buildTokenTimestamps } from "./lib/token-utils" import type { TiktokActions, TiktokAuthValue, TiktokConfig } from "./schema" +const TIKTOK_SCOPES = [ + "user.info.basic", + "user.info.username", + "user.info.profile", + "user.info.stats", + "user.account.type", + "message.list.read", + "message.list.send", + "message.list.manage", +].join(",") + const config: IntegrationDefinition< TiktokConfig, TiktokAuthValue, @@ -28,6 +42,82 @@ const config: IntegrationDefinition< }, }, actions: {}, + connection: { + kind: "channel", + strategy: "oauth_redirect", + multiAccount: true, + configFields: [], + // Bypasses `generateAuthUrl` deliberately: it base64-JSON-encodes + // `stateParams` for the legacy per-request cookie flow, but the + // Connection domain callback hub matches a raw `"{sessionId}.{nonce}"`. + authorizeUrl: ({ credential, callbackUrl, state }) => { + const config = credential as TiktokConfig + const params = new URLSearchParams({ + client_key: config.clientId, + response_type: "code", + scope: TIKTOK_SCOPES, + redirect_uri: callbackUrl, + disable_auto_auth: "1", + state, + }) + return `https://www.tiktok.com/v2/auth/authorize/?${params.toString()}` + }, + exchangeCode: async ({ code, callbackUrl, credential }) => { + const config = credential as TiktokConfig + const tokenResponse = await exchangeCodeForToken( + { + clientId: config.clientId, + clientSecret: config.clientSecret, + redirectUrl: callbackUrl, + }, + code, + ) + const userInfo = await getUserInfo({ + accessToken: tokenResponse.access_token, + }) + + return { + authType: AuthType.oauth2, + clientId: config.clientId, + clientSecret: config.clientSecret, + redirectUrl: "", + tokens: { + accessToken: tokenResponse.access_token, + refreshToken: tokenResponse.refresh_token, + ...buildTokenTimestamps( + tokenResponse.expires_in, + tokenResponse.refresh_expires_in, + ), + }, + metadata: { + openId: tokenResponse.open_id, + username: userInfo.username, + displayName: userInfo.display_name, + }, + } satisfies TiktokAuthValue + }, + describe: (auth) => ({ + sourceId: auth.metadata.openId, + displayName: + auth.metadata.displayName || auth.metadata.username || "TikTok", + }), + verify: async ({ auth }) => { + try { + await getUserInfo({ accessToken: auth.tokens.accessToken }) + return { ok: true, authExpiresAt: auth.tokens.expiresAt } + } catch (error) { + return { + ok: false, + revoked: isRevokedTokenError(error), + error: + error instanceof Error + ? error.message + : "Unable to verify TikTok connection", + } + } + }, + isRevokedTokenError, + }, refreshAuth: async ({ auth }) => { if (!auth.tokens.refreshToken) { throw new AuthException("TikTok refresh token not available") diff --git a/integrations/webchat/src/integration.ts b/integrations/webchat/src/integration.ts index a5275d3bc4..ba0b6a4a0a 100644 --- a/integrations/webchat/src/integration.ts +++ b/integrations/webchat/src/integration.ts @@ -22,6 +22,21 @@ const config: IntegrationDefinition< }, }, actions: {}, + connection: { + kind: "channel", + strategy: "self_serve", + multiAccount: true, + configFields: [], + describe: () => ({ + // Webchat has no external account identity. + sourceId: "workspace", + displayName: "Webchat", + }), + // Webchat is self-hosted, so there is no external provider to verify. + verify: async () => ({ ok: true }), + // TODO(connection-phase2): refine once Webchat revoked-token error shape is confirmed. + isRevokedTokenError: () => false, + }, handleRequest( _props: HandleRequestProps, ): Promise { diff --git a/integrations/whatsapp/src/integration.ts b/integrations/whatsapp/src/integration.ts index 137b9ed507..5febc3d7e8 100644 --- a/integrations/whatsapp/src/integration.ts +++ b/integrations/whatsapp/src/integration.ts @@ -11,12 +11,13 @@ import { updateConversationalAutomation, } from "./api/phone-number" import { listFlows, listMessageTemplates } from "./api/waba" -import { unsubscribeWebhook } from "./api/webhook" +import { subscribeWebhook, unsubscribeWebhook } from "./api/webhook" import { uploadMedia, verifyAccessToken } from "./client" import { botHandlers } from "./handlers/bot" import { conversationHandlers } from "./handlers/conversation" import { messageHandlers } from "./handlers/message" import { webhookHandler } from "./handlers/webhook" +import { isRevokedTokenError } from "./lib/error-mapper" import type { WhatsappActions, WhatsappAuthValue, @@ -52,6 +53,41 @@ const config: IntegrationDefinition< updateConversationalAutomation: async ({ ctx, data }) => await updateConversationalAutomation(ctx.auth, data), }, + connection: { + kind: "channel", + strategy: "oauth_redirect", + multiAccount: true, + configFields: [], + describe: (auth) => ({ + sourceId: auth.metadata.phoneNumber.id, + displayName: + auth.metadata.phoneNumber.verified_name || + auth.metadata.phoneNumber.display_phone_number || + "WhatsApp", + }), + verify: async ({ auth }) => { + try { + await verifyAccessToken({ + auth, + } as Parameters[0]) + return { ok: true, authExpiresAt: auth.tokens.expiresAt } + } catch (error) { + return { + ok: false, + revoked: isRevokedTokenError(error), + error: + error instanceof Error + ? error.message + : "Unable to verify WhatsApp connection", + } + } + }, + isRevokedTokenError, + webhook: { + subscribe: async ({ auth }) => await subscribeWebhook({ auth }), + unsubscribe: async ({ auth }) => await unsubscribeWebhook({ auth }), + }, + }, handleRequest: async (props) => { const segments = new URL(props.req.url).pathname.split("/") diff --git a/integrations/zalo/__tests__/connection-oauth.test.ts b/integrations/zalo/__tests__/connection-oauth.test.ts new file mode 100644 index 0000000000..4b2c483525 --- /dev/null +++ b/integrations/zalo/__tests__/connection-oauth.test.ts @@ -0,0 +1,99 @@ +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest" +import type * as ZaloAuthApi from "../src/api/auth" +import { integration } from "../src/integration" + +const mocks = vi.hoisted(() => ({ + convertCodeToTokens: vi.fn(), + getZaloOAProfile: vi.fn(), +})) + +vi.mock("../src/api/auth", async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + convertCodeToTokens: mocks.convertCodeToTokens, + getZaloOAProfile: mocks.getZaloOAProfile, + } +}) + +const credential = { + clientId: "app-1", + clientSecret: "secret-1", + redirectUrl: "", + version: "v4", +} + +describe("Zalo connection.authorizeUrl", () => { + test("passes state through verbatim, not JSON/base64-wrapped", () => { + const url = integration.connection.authorizeUrl?.({ + credential, + callbackUrl: "https://app.example.test/integrations/zalo/callback", + state: "session-1.abc123", + }) + const parsed = new URL(url as string) + + expect(parsed.searchParams.get("state")).toBe("session-1.abc123") + expect(parsed.searchParams.get("app_id")).toBe("app-1") + expect(parsed.searchParams.get("redirect_uri")).toBe( + "https://app.example.test/integrations/zalo/callback", + ) + expect(parsed.origin).toBe("https://oauth.zaloapp.com") + expect(parsed.pathname).toBe("/v4/oa/permission") + }) +}) + +describe("Zalo connection.exchangeCode", () => { + beforeEach(() => { + vi.useFakeTimers() + vi.setSystemTime(new Date("2026-09-15T12:00:00.000Z")) + vi.clearAllMocks() + mocks.convertCodeToTokens.mockResolvedValue({ + access_token: "access-1", + refresh_token: "refresh-1", + expires_in: 3600, + }) + mocks.getZaloOAProfile.mockResolvedValue({ + oa_id: "oa-1", + name: "Zalo Official Account", + description: "", + avatar: "", + }) + }) + + afterEach(() => { + vi.useRealTimers() + }) + + test("exchanges the code and returns the complete Zalo account auth", async () => { + const callbackUrl = "https://app.example.test/integrations/zalo/callback" + + const auth = await integration.connection.exchangeCode?.({ + code: "auth-code", + callbackUrl, + credential, + }) + + expect(mocks.convertCodeToTokens).toHaveBeenCalledWith( + { ...credential, redirectUrl: callbackUrl }, + "auth-code", + ) + expect(mocks.getZaloOAProfile).toHaveBeenCalledWith("access-1") + expect(auth).toEqual({ + authType: "oauth2", + clientId: "app-1", + clientSecret: "secret-1", + redirectUrl: callbackUrl, + version: "v4", + tokens: { + accessToken: "access-1", + refreshToken: "refresh-1", + expiresAt: "2026-09-15T13:00:00.000Z", + }, + oaId: "oa-1", + metadata: { + version: "v4", + oaName: "Zalo Official Account", + }, + }) + }) +}) diff --git a/integrations/zalo/src/integration.ts b/integrations/zalo/src/integration.ts index 12255b587e..aa163ecaaf 100644 --- a/integrations/zalo/src/integration.ts +++ b/integrations/zalo/src/integration.ts @@ -1,11 +1,16 @@ import { AuthException, + AuthType, HandleRequestType, Integration, type IntegrationDefinition, SdkException, } from "@chatbotx.io/sdk" -import { refreshAccessToken } from "./api/auth" +import { + convertCodeToTokens, + getZaloOAProfile, + refreshAccessToken, +} from "./api/auth" import { getUserDetail, listOaTags, @@ -13,10 +18,12 @@ import { removeTag, tagFollower, } from "./api/tag" +import { ZALO_API_ENDPOINTS, ZALO_OAUTH_BASE_URL } from "./constants" import { callbackHandler } from "./handlers/callback" import { contactHandlers } from "./handlers/handler" import { messageHandlers } from "./handlers/message" import { webhookHandler } from "./handlers/webhook" +import { isRevokedTokenError } from "./lib/error-mapper" import type { ZaloActions, ZaloAuthValue, @@ -39,6 +46,69 @@ const config: IntegrationDefinition = { removeTag, getUserDetail, }, + connection: { + kind: "channel", + strategy: "oauth_redirect", + multiAccount: true, + configFields: [], + // Bypasses `generateAuthUrl` deliberately: that helper base64-JSON- + // encodes `stateParams` for the legacy cookie callback flow, whereas the + // Connection callback hub matches this raw session state verbatim. + authorizeUrl: ({ credential, callbackUrl, state }) => { + const config = credential as ZaloConfig + const params = new URLSearchParams({ + app_id: config.clientId, + redirect_uri: callbackUrl, + state, + }) + return `${ZALO_OAUTH_BASE_URL}${ZALO_API_ENDPOINTS.AUTH.PERMISSION}?${params.toString()}` + }, + exchangeCode: async ({ code, callbackUrl, credential }) => { + const config = credential as ZaloConfig + const tokens = await convertCodeToTokens( + { ...config, redirectUrl: callbackUrl }, + code, + ) + const oaProfile = await getZaloOAProfile(tokens.access_token) + return { + authType: AuthType.oauth2, + clientId: config.clientId, + clientSecret: config.clientSecret, + redirectUrl: callbackUrl, + version: config.version, + tokens: { + accessToken: tokens.access_token, + refreshToken: tokens.refresh_token, + expiresAt: calculateExpiresAt(tokens.expires_in), + }, + oaId: oaProfile.oa_id, + metadata: { + version: config.version, + oaName: oaProfile.name, + }, + } satisfies ZaloAuthValue + }, + describe: (auth) => ({ + sourceId: auth.oaId, + displayName: auth.metadata.oaName || "Zalo", + }), + verify: async ({ auth }) => { + try { + await getZaloOAProfile(auth.tokens.accessToken) + return { ok: true, authExpiresAt: auth.tokens.expiresAt } + } catch (error) { + return { + ok: false, + revoked: isRevokedTokenError(error), + error: + error instanceof Error + ? error.message + : "Unable to verify Zalo connection", + } + } + }, + isRevokedTokenError, + }, handleRequest: async (props) => { const segments = new URL(props.req.url).pathname.split("/") const method = segments.pop() diff --git a/packages/business/__tests__/appointment-service-webview.test.ts b/packages/business/__tests__/appointment-service-webview.test.ts index e9a7a36b47..f8f37d0bda 100644 --- a/packages/business/__tests__/appointment-service-webview.test.ts +++ b/packages/business/__tests__/appointment-service-webview.test.ts @@ -66,7 +66,8 @@ vi.mock("@chatbotx.io/encryption", () => ({ signAppointmentScheduleToken: vi.fn(async () => "schedule-token"), })) -vi.mock("@chatbotx.io/redis", () => ({ +vi.mock("@chatbotx.io/redis", async (importOriginal) => ({ + ...(await importOriginal>()), invalidateCacheByTags: vi.fn(), })) diff --git a/packages/business/__tests__/integration-service.test.ts b/packages/business/__tests__/integration-service.test.ts index 0f69690253..9ea9a4b85b 100644 --- a/packages/business/__tests__/integration-service.test.ts +++ b/packages/business/__tests__/integration-service.test.ts @@ -38,6 +38,10 @@ vi.mock("@chatbotx.io/redis", () => ({ invalidateCacheByTags: vi.fn(), })) +vi.mock("../src/connection/state-service", () => ({ + connectionStateService: { list: vi.fn() }, +})) + const { integrationService } = await import("../src/integration/service") beforeEach(() => { diff --git a/packages/business/package.json b/packages/business/package.json index 3987d58546..a69b6bd272 100644 --- a/packages/business/package.json +++ b/packages/business/package.json @@ -19,6 +19,8 @@ "./contact-sequence": "./src/contact-sequence/index.ts", "./coexist": "./src/coexist/index.ts", "./coexist/history": "./src/coexist/history-window.ts", + "./connect-session": "./src/connect-session/index.ts", + "./connection": "./src/connection/index.ts", "./coupon": "./src/coupon/index.ts", "./dynamic-image": "./src/dynamic-image/index.ts", "./error-log": "./src/error-log/index.ts", @@ -27,6 +29,7 @@ "./inbox/connect-outcome": "./src/inbox/connect-outcome.ts", "./inbox/connect-outcome-types": "./src/inbox/connect-outcome-types.ts", "./inbox/schema": "./src/inbox/schema.ts", + "./integration-ai-provider/verify": "./src/integration-ai-provider/verify.ts", "./license-startup": "./src/enterprise/license/startup.ts", "./javascript-execution": "./src/javascript-execution/index.ts", "./minigame": "./src/minigame/index.ts", @@ -69,6 +72,7 @@ "drizzle-orm": "1.0.0-beta.22", "image-size": "^2.0.2", "jose": "^6.2.3", + "ky": "^2.0.2", "libphonenumber-js": "^1.12.42", "qrcode": "^1.5.4", "remeda": "^2.37.0", diff --git a/packages/business/src/connect-session/__tests__/service.test.ts b/packages/business/src/connect-session/__tests__/service.test.ts new file mode 100644 index 0000000000..b058e939d6 --- /dev/null +++ b/packages/business/src/connect-session/__tests__/service.test.ts @@ -0,0 +1,358 @@ +import { beforeEach, describe, expect, it, vi } from "vitest" + +const mocks = vi.hoisted(() => ({ + countActiveByWorkspaceId: vi.fn(async () => 0), + insert: vi.fn(), + update: vi.fn(), + findById: vi.fn(), + findByIdForWorkspace: vi.fn(), + findByStateNonceHash: vi.fn(), + claimTarget: vi.fn(), + listExpired: vi.fn(async (): Promise[]> => []), +})) + +vi.mock("@chatbotx.io/database/repositories", () => ({ + connectSessionRepository: { + countActiveByWorkspaceId: mocks.countActiveByWorkspaceId, + insert: mocks.insert, + update: mocks.update, + findById: mocks.findById, + findByIdForWorkspace: mocks.findByIdForWorkspace, + findByStateNonceHash: mocks.findByStateNonceHash, + claimTarget: mocks.claimTarget, + listExpired: mocks.listExpired, + }, +})) + +const { connectSessionService } = await import("../service") + +const FUTURE = new Date(Date.now() + 60_000) +const PAST = new Date(Date.now() - 60_000) + +const baseSession = (overrides: Partial> = {}) => ({ + id: "session-1", + workspaceId: "ws-1", + provider: "messenger", + status: "pending", + expiresAt: FUTURE, + targets: [], + results: [], + resultConnectionIds: [], + ...overrides, +}) + +beforeEach(() => { + vi.clearAllMocks() + mocks.countActiveByWorkspaceId.mockResolvedValue(0) + mocks.update.mockImplementation( + async (input: { id: string; values: Record }) => ({ + ...baseSession(), + id: input.id, + ...input.values, + }), + ) +}) + +describe("connectSessionService.create", () => { + it("throws when neither actorUserId nor actorTokenId is provided", async () => { + await expect( + connectSessionService.create({ + workspaceId: "ws-1", + provider: "messenger", + purpose: "connect", + }), + ).rejects.toThrow("exactly one of actorUserId/actorTokenId") + }) + + it("throws when both actorUserId and actorTokenId are provided", async () => { + await expect( + connectSessionService.create({ + workspaceId: "ws-1", + provider: "messenger", + purpose: "connect", + actorUserId: "user-1", + actorTokenId: "token-1", + }), + ).rejects.toThrow("exactly one of actorUserId/actorTokenId") + }) + + it("throws connectSessionLimitReached at the per-workspace pending cap", async () => { + mocks.countActiveByWorkspaceId.mockResolvedValue(20) + await expect( + connectSessionService.create({ + workspaceId: "ws-1", + provider: "messenger", + purpose: "connect", + actorUserId: "user-1", + }), + ).rejects.toMatchObject({ code: "connectSessionLimitReached" }) + expect(mocks.insert).not.toHaveBeenCalled() + }) + + it("mints a nonce whose hash resolves back to the inserted session via findByNonce", async () => { + mocks.insert.mockImplementation( + async (values: Record) => ({ + ...baseSession(), + ...values, + }), + ) + + const { session, nonce } = await connectSessionService.create({ + workspaceId: "ws-1", + provider: "messenger", + purpose: "connect", + actorUserId: "user-1", + }) + + expect(session.status).toBe("pending") + expect(typeof nonce).toBe("string") + expect(nonce).toHaveLength(64) // 32 bytes hex-encoded + + const insertedHash = mocks.insert.mock.calls[0][0].stateNonceHash + mocks.findByStateNonceHash.mockImplementation( + async (input: { stateNonceHash: string }) => + input.stateNonceHash === insertedHash ? session : undefined, + ) + + await expect(connectSessionService.findByNonce(nonce)).resolves.toEqual( + session, + ) + await expect( + connectSessionService.findByNonce("wrong-nonce"), + ).resolves.toBeUndefined() + }) +}) + +describe("expiry rule", () => { + it("lazily flips an active session past expiresAt to expired", async () => { + mocks.findByIdForWorkspace.mockResolvedValue( + baseSession({ expiresAt: PAST, status: "pending" }), + ) + + const result = await connectSessionService.findByIdForWorkspace({ + id: "session-1", + workspaceId: "ws-1", + }) + + expect(result?.status).toBe("expired") + expect(mocks.update).toHaveBeenCalledWith({ + id: "session-1", + values: { status: "expired" }, + }) + }) + + it("does not touch a terminal-status session past expiresAt", async () => { + mocks.findByIdForWorkspace.mockResolvedValue( + baseSession({ expiresAt: PAST, status: "completed" }), + ) + + const result = await connectSessionService.findByIdForWorkspace({ + id: "session-1", + workspaceId: "ws-1", + }) + + expect(result?.status).toBe("completed") + expect(mocks.update).not.toHaveBeenCalled() + }) + + it("returns an unexpired session unchanged", async () => { + mocks.findByIdForWorkspace.mockResolvedValue(baseSession()) + + const result = await connectSessionService.findByIdForWorkspace({ + id: "session-1", + workspaceId: "ws-1", + }) + + expect(result?.status).toBe("pending") + expect(mocks.update).not.toHaveBeenCalled() + }) +}) + +describe("connectSessionService.attachAuthorization", () => { + it("throws connectSessionExpired when the session is not active", async () => { + mocks.findById.mockResolvedValue(baseSession({ status: "completed" })) + + await expect( + connectSessionService.attachAuthorization({ + id: "session-1", + encryptedAuth: { iv: "x", ciphertext: "y", keyId: "k" } as never, + targets: [], + }), + ).rejects.toMatchObject({ code: "connectSessionExpired" }) + }) + + it("moves an active session to awaiting_selection with the given targets", async () => { + mocks.findById.mockResolvedValue(baseSession({ status: "pending" })) + + const targets = [{ id: "page-1", name: "Page One", selectable: true }] + await connectSessionService.attachAuthorization({ + id: "session-1", + encryptedAuth: { iv: "x", ciphertext: "y", keyId: "k" } as never, + targets, + }) + + expect(mocks.update).toHaveBeenCalledWith( + expect.objectContaining({ + id: "session-1", + values: expect.objectContaining({ + status: "awaiting_selection", + step: "select", + targets, + }), + }), + ) + }) +}) + +describe("connectSessionService.claimTarget", () => { + it("forwards to the repository's atomic claim", async () => { + mocks.claimTarget.mockResolvedValue(true) + await expect( + connectSessionService.claimTarget({ + id: "session-1", + targetId: "page-1", + }), + ).resolves.toBe(true) + expect(mocks.claimTarget).toHaveBeenCalledWith({ + id: "session-1", + targetId: "page-1", + }) + }) + + it("returns false when the target was already claimed (race lost)", async () => { + mocks.claimTarget.mockResolvedValue(false) + await expect( + connectSessionService.claimTarget({ + id: "session-1", + targetId: "page-1", + }), + ).resolves.toBe(false) + }) +}) + +describe("connectSessionService.recordResults", () => { + it("stays awaiting_selection until every target has a result", async () => { + mocks.findById.mockResolvedValue( + baseSession({ + status: "awaiting_selection", + targets: [{ id: "a" }, { id: "b" }], + results: [], + resultConnectionIds: [], + }), + ) + + const result = await connectSessionService.recordResults({ + id: "session-1", + results: [{ targetId: "a", status: "connected", connectionId: "c1" }], + resultConnectionIds: ["c1"], + }) + + expect(result.status).not.toBe("completed") + expect(mocks.update).toHaveBeenCalledWith( + expect.objectContaining({ + values: expect.objectContaining({ status: "awaiting_selection" }), + }), + ) + }) + + it("marks completed once accumulated results cover every target", async () => { + mocks.findById.mockResolvedValue( + baseSession({ + status: "awaiting_selection", + targets: [{ id: "a" }, { id: "b" }], + results: [{ targetId: "a", status: "connected", connectionId: "c1" }], + resultConnectionIds: ["c1"], + }), + ) + + const result = await connectSessionService.recordResults({ + id: "session-1", + results: [{ targetId: "b", status: "connected", connectionId: "c2" }], + resultConnectionIds: ["c2"], + }) + + expect(result.status).toBe("completed") + expect(mocks.update).toHaveBeenCalledWith( + expect.objectContaining({ + values: expect.objectContaining({ + status: "completed", + resultConnectionIds: ["c1", "c2"], + }), + }), + ) + }) +}) + +describe("connectSessionService.fail / cancel", () => { + it("fail sets status failed with the given errorCode", async () => { + const result = await connectSessionService.fail({ + id: "session-1", + errorCode: "provider_denied", + }) + expect(result.status).toBe("failed") + expect(result.errorCode).toBe("provider_denied") + }) + + it("cancel requires the session to belong to the workspace", async () => { + mocks.findByIdForWorkspace.mockResolvedValue(undefined) + await expect( + connectSessionService.cancel({ id: "session-1", workspaceId: "ws-1" }), + ).rejects.toMatchObject({ code: "notFound" }) + }) + + it("cancel sets status cancelled for an owned session", async () => { + mocks.findByIdForWorkspace.mockResolvedValue(baseSession()) + const result = await connectSessionService.cancel({ + id: "session-1", + workspaceId: "ws-1", + }) + expect(result.status).toBe("cancelled") + }) +}) + +describe("connectSessionService.purgeExpired", () => { + it("flips every session listExpired returns to expired and reports the count", async () => { + mocks.listExpired.mockResolvedValue([ + baseSession({ id: "s1" }), + baseSession({ id: "s2" }), + ]) + + const count = await connectSessionService.purgeExpired() + + expect(count).toBe(2) + expect(mocks.update).toHaveBeenCalledWith({ + id: "s1", + values: { status: "expired" }, + }) + expect(mocks.update).toHaveBeenCalledWith({ + id: "s2", + values: { status: "expired" }, + }) + }) +}) + +describe("connectSessionService.updateReturnUrl", () => { + it("sets returnUrl on the given session, referencing its own id", async () => { + const result = await connectSessionService.updateReturnUrl({ + id: "session-1", + returnUrl: "/channels/messenger/select?session=session-1", + }) + expect(result.returnUrl).toBe( + "/channels/messenger/select?session=session-1", + ) + expect(mocks.update).toHaveBeenCalledWith({ + id: "session-1", + values: { returnUrl: "/channels/messenger/select?session=session-1" }, + }) + }) + + it("throws notFound when the session does not exist", async () => { + mocks.update.mockResolvedValue(undefined) + await expect( + connectSessionService.updateReturnUrl({ + id: "missing", + returnUrl: "/channels/messenger/select?session=missing", + }), + ).rejects.toMatchObject({ code: "notFound" }) + }) +}) diff --git a/packages/business/src/connect-session/index.ts b/packages/business/src/connect-session/index.ts new file mode 100644 index 0000000000..9376fea807 --- /dev/null +++ b/packages/business/src/connect-session/index.ts @@ -0,0 +1 @@ +export * from "./service" diff --git a/packages/business/src/connect-session/service.ts b/packages/business/src/connect-session/service.ts new file mode 100644 index 0000000000..3ab0054a60 --- /dev/null +++ b/packages/business/src/connect-session/service.ts @@ -0,0 +1,374 @@ +import type { + ConnectSessionErrorCode, + ConnectSessionOutcome, + ConnectSessionPurpose, + ConnectSessionStatus, + ConnectSessionTarget, + IntegrationType, +} from "@chatbotx.io/database/partials" +import { connectSessionRepository } from "@chatbotx.io/database/repositories" +import type { ConnectSessionModel } from "@chatbotx.io/database/types" +import type { EncryptedData } from "@chatbotx.io/encryption" +import { BaseService } from "../base.service" +import { ChatbotXException, connectSessionExpiredException } from "../errors" + +/** 10 min to complete the OAuth/credential round trip before the session goes stale. */ +const PENDING_TTL_MS = 10 * 60 * 1000 +/** 30 min from authorization to finish target selection once the provider has granted access. */ +const AUTHORIZED_TTL_MS = 30 * 60 * 1000 +const NONCE_BYTES = 32 +/** Caps concurrent in-flight sessions per workspace — a runaway client retrying `create` cannot exhaust the table. */ +const MAX_PENDING_SESSIONS_PER_WORKSPACE = 20 + +const ACTIVE_STATUSES: ReadonlySet = new Set([ + "pending", + "authorized", + "awaiting_selection", +] satisfies ConnectSessionStatus[]) + +const randomBytes = (length: number): Uint8Array => + crypto.getRandomValues(new Uint8Array(length)) + +const toHex = (bytes: Uint8Array): string => + Array.from(bytes) + .map((byte) => byte.toString(16).padStart(2, "0")) + .join("") + +/** Web Crypto only — safe in both Node and edge runtimes, same primitive as `workspace-api-token/credentials.ts#hashToken`. */ +const hashNonce = async (nonce: string): Promise => { + const data = new TextEncoder().encode(nonce) + const digest = await crypto.subtle.digest("SHA-256", data) + return toHex(new Uint8Array(digest)) +} + +export class ConnectSessionNotFoundException extends ChatbotXException { + constructor() { + super("Connect session not found.", "notFound", 404) + } +} + +const sessionLimitReachedException = () => + new ChatbotXException( + "Too many pending connect sessions for this workspace. Complete or cancel an existing one first.", + "connectSessionLimitReached", + 429, + ) + +/** Exactly one of `actorUserId`/`actorTokenId` — mirrors the `ConnectSession_actor_exactly_one` DB CHECK constraint so a violation surfaces before the insert, not as a raw constraint-violation error. */ +const requireExactlyOneActor = (input: { + actorUserId?: string | null + actorTokenId?: string | null +}): void => { + const hasUser = Boolean(input.actorUserId) + const hasToken = Boolean(input.actorTokenId) + if (hasUser === hasToken) { + throw new Error( + "ConnectSession requires exactly one of actorUserId/actorTokenId", + ) + } +} + +/** + * DB-backed reads/writes over the `ConnectSession` table — the multi-step + * OAuth/credential connect flow record (Home Assistant config-flow / + * Nango-connect-session shaped: a `nextAction` field the client renders, + * advanced by server-driven steps rather than a client-owned state machine). + * + * Deliberately **registry-free**, mirroring `connectionStateService`: it + * never imports `@chatbotx.io/connections`, so it stays safe to call from + * the OAuth callback hub and the completion page's private API without + * pulling in the full provider registry. The registry-aware orchestration + * (provider `authorizeUrl`/`exchangeCode`/`listCandidates`/`connect` calls) + * lives in `ConnectionService.startSession`/`completeAuthorization`/ + * `connectTargets` (`@chatbotx.io/connections`), which call this service for + * every state read/write. + */ +class ConnectSessionService extends BaseService { + /** Mints a new session and its one-time plaintext nonce (never persisted — only its hash is). Throws `connectSessionLimitReached` past the per-workspace pending cap. */ + async create(input: { + workspaceId: string + provider: IntegrationType + purpose: ConnectSessionPurpose + targetConnectionId?: string | null + actorUserId?: string | null + actorTokenId?: string | null + platformOwnerId?: string | null + originHost?: string | null + returnUrl?: string | null + }): Promise<{ session: ConnectSessionModel; nonce: string }> { + requireExactlyOneActor(input) + + const activeCount = await connectSessionRepository.countActiveByWorkspaceId( + { workspaceId: input.workspaceId }, + ) + if (activeCount >= MAX_PENDING_SESSIONS_PER_WORKSPACE) { + throw sessionLimitReachedException() + } + + const nonce = toHex(randomBytes(NONCE_BYTES)) + const stateNonceHash = await hashNonce(nonce) + + const session = await connectSessionRepository.insert({ + workspaceId: input.workspaceId, + provider: input.provider, + purpose: input.purpose, + targetConnectionId: input.targetConnectionId ?? null, + actorUserId: input.actorUserId ?? null, + actorTokenId: input.actorTokenId ?? null, + platformOwnerId: input.platformOwnerId ?? null, + originHost: input.originHost ?? null, + returnUrl: input.returnUrl ?? null, + stateNonceHash, + status: "pending", + step: "authorize", + expiresAt: new Date(Date.now() + PENDING_TTL_MS), + }) + + return { session, nonce } + } + + /** Resolves a session by its plaintext OAuth `state` nonce — the only lookup the callback hub can do before it knows a workspace. Applies the `expiresAt` rule before returning. */ + async findByNonce(nonce: string): Promise { + const stateNonceHash = await hashNonce(nonce) + const session = await connectSessionRepository.findByStateNonceHash({ + stateNonceHash, + }) + return await this.applyExpiryRule(session) + } + + async findByIdForWorkspace(input: { + id: string + workspaceId: string + }): Promise { + const session = await connectSessionRepository.findByIdForWorkspace(input) + return await this.applyExpiryRule(session) + } + + /** + * Workspace-unscoped lookup by id alone — the `/connect/{id}` completion + * page has no builder session (the person completing an API/MCP-started + * OAuth connect is never necessarily logged into the builder, or even a + * member of the workspace that started it). Safe to expose unscoped: the + * id is an unguessable snowflake acting as its own capability token, and + * the returned row's public projection (`ConnectSessionResource`) never + * carries `encryptedAuth`/`stateNonceHash`/`claimedTargetIds`. + */ + async findById(id: string): Promise { + const session = await connectSessionRepository.findById({ id }) + return await this.applyExpiryRule(session) + } + + /** Same as `findByIdForWorkspace`, throwing instead of returning `undefined` — the shape most callers actually want. */ + async requireByIdForWorkspace(input: { + id: string + workspaceId: string + }): Promise { + const session = await this.findByIdForWorkspace(input) + if (!session) { + throw new ConnectSessionNotFoundException() + } + return session + } + + /** + * Sets `returnUrl` on an already-created session — for a caller (a + * builder picker's OAuth-initiating route) that needs the redirect target + * to reference the session's own id (`?session={id}`), which isn't known + * until after `create()` returns. Best-effort by design at the call site: + * the OAuth dialog hasn't been visited yet when this runs, so there is no + * risk of a lost update racing a completed authorization. + */ + async updateReturnUrl(input: { + id: string + returnUrl: string + }): Promise { + const updated = await connectSessionRepository.update({ + id: input.id, + values: { returnUrl: input.returnUrl }, + }) + if (!updated) { + throw new ConnectSessionNotFoundException() + } + return updated + } + + /** + * Persists the exchanged auth and the provider's candidate list, moving + * the session to `awaiting_selection`. The caller (registry-aware + * `ConnectionService.completeAuthorization`) decides whether to + * immediately follow with `connectTargets` for a single-target/ + * non-multi-account provider — this method itself makes no registry-aware + * decision, it only records the step. + */ + async attachAuthorization(input: { + id: string + encryptedAuth: EncryptedData + targets: ConnectSessionTarget[] + }): Promise { + const existing = await this.requireActive(input.id) + const updated = await connectSessionRepository.update({ + id: existing.id, + values: { + status: "awaiting_selection", + step: "select", + encryptedAuth: input.encryptedAuth, + targets: input.targets, + expiresAt: new Date(Date.now() + AUTHORIZED_TTL_MS), + }, + }) + if (!updated) { + throw new ConnectSessionNotFoundException() + } + return updated + } + + /** + * Atomic per-target claim — the compare-and-set that makes concurrent + * `connectTargets` calls (a double-submit, or two tabs) safe. Returns + * `false` when the target was already claimed by a prior call on this + * session; the caller maps that to a `duplicated` outcome rather than + * connecting the same target twice. + */ + async claimTarget(input: { id: string; targetId: string }): Promise { + return await connectSessionRepository.claimTarget(input) + } + + /** + * Merges outcomes/connection ids into the session's running totals and, + * once every originally-offered target has a result, marks the session + * `completed`. Safe to call more than once for the same session (e.g. + * `connectTargets` processing two batches) — results accumulate rather + * than overwrite. + */ + async recordResults(input: { + id: string + results: ConnectSessionOutcome[] + resultConnectionIds: string[] + }): Promise { + const existing = await connectSessionRepository.findById({ id: input.id }) + if (!existing) { + throw new ConnectSessionNotFoundException() + } + const mergedResults = [...existing.results, ...input.results] + const mergedConnectionIds = [ + ...existing.resultConnectionIds, + ...input.resultConnectionIds, + ] + const isComplete = mergedResults.length >= existing.targets.length + const updated = await connectSessionRepository.update({ + id: existing.id, + values: { + status: isComplete ? "completed" : existing.status, + step: isComplete ? "done" : existing.step, + results: mergedResults, + resultConnectionIds: mergedConnectionIds, + consumedAt: isComplete ? new Date() : existing.consumedAt, + }, + }) + if (!updated) { + throw new ConnectSessionNotFoundException() + } + return updated + } + + /** Records user-submitted `enter_input` step data (e.g. a credential-strategy `config`) without changing status — the caller advances the step separately once it has processed the input. */ + async submitInput(input: { + id: string + nextAction: ConnectSessionModel["nextAction"] + }): Promise { + await this.requireActive(input.id) + const updated = await connectSessionRepository.update({ + id: input.id, + values: { nextAction: input.nextAction }, + }) + if (!updated) { + throw new ConnectSessionNotFoundException() + } + return updated + } + + async fail(input: { + id: string + errorCode: ConnectSessionErrorCode + }): Promise { + const updated = await connectSessionRepository.update({ + id: input.id, + values: { + status: "failed", + errorCode: input.errorCode, + consumedAt: new Date(), + }, + }) + if (!updated) { + throw new ConnectSessionNotFoundException() + } + return updated + } + + async cancel(input: { + id: string + workspaceId: string + }): Promise { + const existing = await this.requireByIdForWorkspace(input) + const updated = await connectSessionRepository.update({ + id: existing.id, + values: { status: "cancelled", consumedAt: new Date() }, + }) + if (!updated) { + throw new ConnectSessionNotFoundException() + } + return updated + } + + /** Sweeps every active session past `expiresAt` to `status = "expired"` — the `purgeExpired` cron. Returns the number of rows updated. */ + async purgeExpired(): Promise { + const expired = await connectSessionRepository.listExpired({ + before: new Date(), + statuses: [...ACTIVE_STATUSES], + }) + for (const session of expired) { + await connectSessionRepository.update({ + id: session.id, + values: { status: "expired" }, + }) + } + return expired.length + } + + /** Loads an active (non-expired, non-terminal) session by id alone, or throws `connectSessionExpired`. */ + private async requireActive(id: string): Promise { + const session = await connectSessionRepository.findById({ id }) + const row = await this.applyExpiryRule(session) + if (!row) { + throw new ConnectSessionNotFoundException() + } + if (!ACTIVE_STATUSES.has(row.status)) { + throw connectSessionExpiredException( + "This connect session is no longer active.", + ) + } + return row + } + + /** `expiresAt <= now()` reads as `expired` regardless of the stored status — lazily flips the row so every reader agrees without a cron dependency. */ + private async applyExpiryRule( + session: ConnectSessionModel | undefined, + ): Promise { + if (!session) { + return + } + if (session.expiresAt.getTime() > Date.now()) { + return session + } + if (!ACTIVE_STATUSES.has(session.status)) { + return session + } + const updated = await connectSessionRepository.update({ + id: session.id, + values: { status: "expired" }, + }) + return updated ?? session + } +} + +export const connectSessionService = new ConnectSessionService() diff --git a/packages/business/src/connection/__tests__/state-service.test.ts b/packages/business/src/connection/__tests__/state-service.test.ts new file mode 100644 index 0000000000..bd892058c2 --- /dev/null +++ b/packages/business/src/connection/__tests__/state-service.test.ts @@ -0,0 +1,255 @@ +import { beforeEach, describe, expect, test, vi } from "vitest" + +const mocks = vi.hoisted(() => ({ + findById: vi.fn(), + findByProviderAndSourceIdAnyWorkspace: vi.fn(), + update: vi.fn(), + inboxUpdate: vi.fn(), + inboxUpdateSet: vi.fn(), + inboxUpdateWhere: vi.fn(), + tryConsume: vi.fn(), + release: vi.fn(async () => undefined), + increment: vi.fn(async () => undefined), + decrement: vi.fn(async () => undefined), +})) + +vi.mock("@chatbotx.io/database/repositories", () => ({ + connectionRepository: { + findById: mocks.findById, + findByProviderAndSourceIdAnyWorkspace: + mocks.findByProviderAndSourceIdAnyWorkspace, + update: mocks.update, + list: vi.fn(), + count: vi.fn(), + listDueForRefresh: vi.fn(), + }, +})) + +vi.mock("@chatbotx.io/database/client", () => ({ + db: { + update: mocks.inboxUpdate, + }, + eq: vi.fn((column, value) => ({ column, value })), +})) + +vi.mock("@chatbotx.io/database/schema", () => ({ + inboxModel: { id: "id" }, + workspaceUsageModel: {}, +})) + +vi.mock("../../quota-enforcement/service", () => ({ + quotaEnforcementService: { + tryConsume: mocks.tryConsume, + release: mocks.release, + }, +})) + +vi.mock("../../workspace-usage/service", () => ({ + workspaceUsageService: { + increment: mocks.increment, + decrement: mocks.decrement, + }, +})) + +const { connectionStateService } = await import("../state-service") + +const baseConnection = (overrides: Partial> = {}) => ({ + id: "conn-1", + workspaceId: "ws-1", + provider: "messenger", + kind: "channel", + channel: "messenger", + inboxId: "inbox-1", + integrationId: null, + sourceId: "page-1", + displayName: "My Page", + status: "needs_reauth", + statusReason: "token_revoked", + lastError: null, + authExpiresAt: null, + createdBy: null, + connectedAt: null, + disconnectedAt: new Date(), + ...overrides, +}) + +beforeEach(() => { + mocks.findById.mockReset() + mocks.findByProviderAndSourceIdAnyWorkspace.mockReset() + mocks.update.mockReset() + mocks.inboxUpdate.mockReset() + mocks.inboxUpdateSet.mockReset() + mocks.inboxUpdateWhere.mockReset() + mocks.tryConsume.mockReset() + mocks.release.mockClear() + mocks.increment.mockClear() + mocks.decrement.mockClear() + + mocks.inboxUpdateSet.mockImplementation(() => ({ + where: mocks.inboxUpdateWhere, + })) + mocks.inboxUpdate.mockImplementation(() => ({ set: mocks.inboxUpdateSet })) + mocks.inboxUpdateWhere.mockResolvedValue(undefined) + mocks.tryConsume.mockResolvedValue({ ok: true }) +}) + +describe("ConnectionStateService.transition", () => { + test("connect.completed from needs_reauth consumes quota once and mirrors Inbox to connected", async () => { + mocks.findById.mockResolvedValue(baseConnection({ status: "needs_reauth" })) + mocks.update.mockResolvedValue(baseConnection({ status: "connected" })) + + const result = await connectionStateService.transition({ + connectionId: "conn-1", + event: "connect.completed", + ownerId: "owner-1", + }) + + expect(result.status).toBe("connected") + expect(mocks.tryConsume).toHaveBeenCalledTimes(1) + expect(mocks.tryConsume).toHaveBeenCalledWith({ + userId: "owner-1", + metric: "channels", + }) + expect(mocks.release).not.toHaveBeenCalled() + expect(mocks.increment).toHaveBeenCalledWith("ws-1", "channels") + expect(mocks.decrement).not.toHaveBeenCalled() + expect(mocks.inboxUpdateSet).toHaveBeenCalledWith( + expect.objectContaining({ status: "connected" }), + ) + }) + + test("throws ConnectionNotFoundException-style error when the row does not exist", async () => { + mocks.findById.mockResolvedValue(undefined) + + await expect( + connectionStateService.transition({ + connectionId: "missing", + event: "connect.completed", + }), + ).rejects.toThrow("missing") + }) + + test("never consumes quota for a kind:integration connection even when ownerId is passed", async () => { + mocks.findById.mockResolvedValue( + baseConnection({ + kind: "integration", + channel: null, + inboxId: null, + integrationId: "int-1", + status: "disconnected", + }), + ) + mocks.update.mockResolvedValue( + baseConnection({ kind: "integration", status: "connected" }), + ) + + const result = await connectionStateService.transition({ + connectionId: "conn-1", + event: "connect.completed", + ownerId: "owner-1", + }) + + expect(result.status).toBe("connected") + expect(mocks.tryConsume).not.toHaveBeenCalled() + expect(mocks.release).not.toHaveBeenCalled() + }) +}) + +describe("ConnectionStateService.markUnhealthy", () => { + test("releases quota exactly once from an ACTIVE connection and mirrors Inbox to disconnected(token_revoked)", async () => { + mocks.findById.mockResolvedValue(baseConnection({ status: "connected" })) + mocks.update.mockResolvedValue(baseConnection({ status: "needs_reauth" })) + + const result = await connectionStateService.markUnhealthy({ + connectionId: "conn-1", + ownerId: "owner-1", + }) + + expect(result.status).toBe("needs_reauth") + expect(mocks.release).toHaveBeenCalledTimes(1) + expect(mocks.release).toHaveBeenCalledWith({ + userId: "owner-1", + metric: "channels", + }) + expect(mocks.decrement).toHaveBeenCalledWith("ws-1", "channels") + expect(mocks.increment).not.toHaveBeenCalled() + expect(mocks.tryConsume).not.toHaveBeenCalled() + expect(mocks.inboxUpdateSet).toHaveBeenCalledWith( + expect.objectContaining({ + status: "disconnected", + disconnectReason: "token_revoked", + }), + ) + }) + + test("is an idempotent no-op (no quota release) when the connection is already INACTIVE", async () => { + mocks.findById.mockResolvedValue(baseConnection({ status: "needs_reauth" })) + mocks.update.mockResolvedValue(baseConnection({ status: "needs_reauth" })) + + await connectionStateService.markUnhealthy({ + connectionId: "conn-1", + ownerId: "owner-1", + }) + + expect(mocks.release).not.toHaveBeenCalled() + expect(mocks.tryConsume).not.toHaveBeenCalled() + }) +}) + +describe("ConnectionStateService.markUnhealthyByIdentifier", () => { + test("returns null and never touches quota when no connection matches the identifier", async () => { + mocks.findByProviderAndSourceIdAnyWorkspace.mockResolvedValue(undefined) + + const result = await connectionStateService.markUnhealthyByIdentifier({ + provider: "tiktok", + identifier: "open-id-1", + }) + + expect(result).toBeNull() + expect(mocks.findById).not.toHaveBeenCalled() + expect(mocks.release).not.toHaveBeenCalled() + }) + + test("delegates to markUnhealthy when a connection matches", async () => { + mocks.findByProviderAndSourceIdAnyWorkspace.mockResolvedValue( + baseConnection({ id: "conn-2", status: "connected" }), + ) + mocks.findById.mockResolvedValue( + baseConnection({ id: "conn-2", status: "connected" }), + ) + mocks.update.mockResolvedValue( + baseConnection({ id: "conn-2", status: "needs_reauth" }), + ) + + const result = await connectionStateService.markUnhealthyByIdentifier({ + provider: "tiktok", + identifier: "open-id-1", + ownerId: "owner-1", + }) + + expect(result?.status).toBe("needs_reauth") + expect(mocks.release).toHaveBeenCalledTimes(1) + }) +}) + +describe("ConnectionStateService.recordAuthSaved", () => { + test("clears lastError, sets authExpiresAt, and transitions degraded back to connected with no quota change", async () => { + mocks.update.mockResolvedValueOnce(undefined) + mocks.findById.mockResolvedValue(baseConnection({ status: "degraded" })) + mocks.update.mockResolvedValueOnce(baseConnection({ status: "connected" })) + + const expiresAt = new Date("2026-01-01T00:00:00Z") + const result = await connectionStateService.recordAuthSaved({ + connectionId: "conn-1", + authExpiresAt: expiresAt, + }) + + expect(result.status).toBe("connected") + expect(mocks.update.mock.calls[0][0]).toEqual({ + id: "conn-1", + values: { authExpiresAt: expiresAt, lastError: null }, + }) + expect(mocks.tryConsume).not.toHaveBeenCalled() + expect(mocks.release).not.toHaveBeenCalled() + }) +}) diff --git a/packages/business/src/connection/__tests__/state.test.ts b/packages/business/src/connection/__tests__/state.test.ts new file mode 100644 index 0000000000..91447de3a7 --- /dev/null +++ b/packages/business/src/connection/__tests__/state.test.ts @@ -0,0 +1,189 @@ +import { describe, expect, test } from "vitest" +import { + type ConnectionEvent, + isActiveConnectionStatus, + transitionConnection, +} from "../state" + +describe("isActiveConnectionStatus", () => { + test("connected and degraded are ACTIVE; needs_reauth, paused, disconnected are not", () => { + expect(isActiveConnectionStatus("connected")).toBe(true) + expect(isActiveConnectionStatus("degraded")).toBe(true) + expect(isActiveConnectionStatus("needs_reauth")).toBe(false) + expect(isActiveConnectionStatus("paused")).toBe(false) + expect(isActiveConnectionStatus("disconnected")).toBe(false) + }) +}) + +describe("transitionConnection — quota edge invariant", () => { + test("connect.completed from absent/disconnected/needs_reauth consumes quota exactly once", () => { + for (const from of [undefined, "disconnected", "needs_reauth"] as const) { + const result = transitionConnection({ from, event: "connect.completed" }) + expect(result.to).toBe("connected") + expect(result.quotaEdge).toBe("consume") + } + }) + + test("connect.completed from an already-ACTIVE status is an idempotent no-op with no quota change", () => { + for (const from of ["connected", "degraded"] as const) { + const result = transitionConnection({ from, event: "connect.completed" }) + expect(result.to).toBe(from) + expect(result.noop).toBe(true) + expect(result.quotaEdge).toBeNull() + } + }) + + test("user.disconnect from an ACTIVE status releases quota exactly once", () => { + for (const from of ["connected", "degraded"] as const) { + const result = transitionConnection({ from, event: "user.disconnect" }) + expect(result.to).toBe("disconnected") + expect(result.quotaEdge).toBe("release") + } + }) + + test("user.disconnect from an already-INACTIVE status changes nothing quota-wise", () => { + for (const from of ["needs_reauth", "paused", "disconnected"] as const) { + const result = transitionConnection({ from, event: "user.disconnect" }) + expect(result.quotaEdge).toBeNull() + } + }) + + test("auth.revoked from ACTIVE releases quota and lands on needs_reauth", () => { + for (const from of ["connected", "degraded"] as const) { + const result = transitionConnection({ from, event: "auth.revoked" }) + expect(result.to).toBe("needs_reauth") + expect(result.quotaEdge).toBe("release") + expect(result.reason).toBe("token_revoked") + } + }) + + test("auth.revoked against an already-INACTIVE status is an idempotent no-op", () => { + for (const from of ["needs_reauth", "paused", "disconnected"] as const) { + const result = transitionConnection({ from, event: "auth.revoked" }) + expect(result.to).toBe(from) + expect(result.quotaEdge).toBeNull() + } + }) + + test("refresh.transient_failure degrades an ACTIVE connection with no quota change", () => { + for (const from of ["connected", "degraded"] as const) { + const result = transitionConnection({ + from, + event: "refresh.transient_failure", + }) + expect(result.to).toBe("degraded") + expect(result.quotaEdge).toBeNull() + } + }) + + test("refresh/verify events against an INACTIVE status throw (409 CONNECTION_INACTIVE at the API layer)", () => { + for (const from of ["needs_reauth", "paused", "disconnected"] as const) { + expect(() => + transitionConnection({ from, event: "refresh.transient_failure" }), + ).toThrow() + expect(() => transitionConnection({ from, event: "verify.ok" })).toThrow() + expect(() => + transitionConnection({ from, event: "verify.failed_non_auth" }), + ).toThrow() + } + }) + + test("auth.saved / verify.ok clear degraded back to connected with no quota change", () => { + for (const event of ["auth.saved", "verify.ok"] as const) { + const result = transitionConnection({ from: "degraded", event }) + expect(result.to).toBe("connected") + expect(result.quotaEdge).toBeNull() + } + }) + + test("teardown.pause releases quota from ACTIVE and is a no-op from INACTIVE", () => { + for (const from of ["connected", "degraded"] as const) { + const result = transitionConnection({ from, event: "teardown.pause" }) + expect(result.to).toBe("paused") + expect(result.quotaEdge).toBe("release") + } + for (const from of ["needs_reauth", "paused", "disconnected"] as const) { + const result = transitionConnection({ from, event: "teardown.pause" }) + expect(result.quotaEdge).toBeNull() + } + }) + + test("teardown.resume from paused consumes quota; from any other status is a no-op", () => { + const resumed = transitionConnection({ + from: "paused", + event: "teardown.resume", + }) + expect(resumed.to).toBe("connected") + expect(resumed.quotaEdge).toBe("consume") + + for (const from of [ + "connected", + "degraded", + "needs_reauth", + "disconnected", + ] as const) { + const result = transitionConnection({ from, event: "teardown.resume" }) + expect(result.to).toBe(from) + expect(result.quotaEdge).toBeNull() + } + }) + + test("teardown.disconnect always lands on disconnected(workspace_purge) by default, releasing quota exactly when ACTIVE", () => { + for (const from of ["connected", "degraded"] as const) { + const result = transitionConnection({ + from, + event: "teardown.disconnect", + }) + expect(result.to).toBe("disconnected") + expect(result.reason).toBe("workspace_purge") + expect(result.quotaEdge).toBe("release") + } + for (const from of ["needs_reauth", "paused", "disconnected"] as const) { + const result = transitionConnection({ + from, + event: "teardown.disconnect", + }) + expect(result.quotaEdge).toBeNull() + } + }) + + test("every event/from combination in the state table resolves without throwing, except the documented refresh/verify-on-INACTIVE cases", () => { + const allEvents: ConnectionEvent[] = [ + "connect.completed", + "auth.saved", + "refresh.transient_failure", + "verify.failed_non_auth", + "verify.ok", + "auth.revoked", + "user.disconnect", + "teardown.pause", + "teardown.resume", + "teardown.disconnect", + ] + const allStatuses = [ + undefined, + "connected", + "degraded", + "needs_reauth", + "paused", + "disconnected", + ] as const + const throwsOnInactive = new Set([ + "refresh.transient_failure", + "verify.failed_non_auth", + "verify.ok", + "auth.saved", + ]) + + for (const from of allStatuses) { + for (const event of allEvents) { + const isInactive = from === undefined || !isActiveConnectionStatus(from) + if (isInactive && throwsOnInactive.has(event)) { + expect(() => transitionConnection({ from, event })).toThrow() + } else { + expect(() => transitionConnection({ from, event })).not.toThrow() + } + } + } + }) +}) diff --git a/packages/business/src/connection/adapter.ts b/packages/business/src/connection/adapter.ts new file mode 100644 index 0000000000..4c8ec6eeea --- /dev/null +++ b/packages/business/src/connection/adapter.ts @@ -0,0 +1,57 @@ +import type { + CredentialType, + IntegrationType, +} from "@chatbotx.io/database/partials" +import type { + ConnectionProvider, + Integration, + IntegrationDefinition, +} from "@chatbotx.io/sdk" +import type { ConnectionStoreBinding } from "./store-bindings" + +/** + * Everything the Connection domain needs for one `IntegrationType`: the + * runtime `Integration` wrapper (refresh/disconnect), the provider's + * connect/verify/webhook adapter, and its DB store binding. + * `@chatbotx.io/connections` assembles `Record` + * from each `integrations/`'s `connection` export plus + * `CONNECTION_STORE_BINDINGS` — `null` for types with no connect lifecycle + * yet (e.g. `chatbotx`, `metaCatalog` before it gets an `integrations/` + * package). + */ +export type ConnectionAdapter = { + /** + * Present for every provider backed by an `integrations/` SDK + * package (channels + the OAuth2 marketing/calendar integrations). + * Absent for credential-only AI-key providers (`claude`, `deepseek`, + * `gemini`, `openrouter`, `openai`, `openaiCompatible`) that have no + * `integrations/` package — their `ConnectionProvider.verify`/ + * `fromCredentials` call the provider API directly, with no SDK-level + * message/webhook dispatch to wrap. + */ + // biome-ignore lint/suspicious/noExplicitAny: definition generics vary per provider + integration?: Integration> + /** + * Each provider's `ConnectionProvider` is + * narrower than this field's declared type — same heterogeneous-registry + * tradeoff as `integration` above (mirrors how `apps/builder/src/integration.ts` + * already combines 22 distinctly-typed `Integration` instances into one + * object). Callers load the matching row's `auth` for this exact provider, + * so the narrowing is safe at the call site even though it isn't expressed + * in this shared type. + */ + // biome-ignore lint/suspicious/noExplicitAny: heterogeneous registry, see comment above + provider: ConnectionProvider + /** + * Absent only for `chatbotx` — the internal built-in channel has no + * `Integration` satellite table; the `Inbox` row itself is the + * whole connection, so there is nothing for a store binding to load/insert. + */ + store?: ConnectionStoreBinding + credentialType?: CredentialType +} + +export type ConnectionRegistry = Record< + IntegrationType, + ConnectionAdapter | null +> diff --git a/packages/business/src/connection/credential-providers.ts b/packages/business/src/connection/credential-providers.ts new file mode 100644 index 0000000000..8eddf73b71 --- /dev/null +++ b/packages/business/src/connection/credential-providers.ts @@ -0,0 +1,154 @@ +import { + AuthType, + type ConnectionProvider, + type SecretTextAuthValue, +} from "@chatbotx.io/sdk" +import ky, { HTTPError } from "ky" +import { + type AiKeyProvider, + verifyAiProviderApiKey, +} from "../integration-ai-provider/verify" + +/** + * `claude`/`deepseek`/`gemini`/`openai`/`openrouter`/`openaiCompatible` have + * no `integrations/` SDK package (see `packages/connections`'s + * registry note) — they are workspace-scoped API-key credentials with no + * inbound webhook or message dispatch. Their `ConnectionProvider` lives here + * instead of a `connection` field on an `IntegrationDefinition`. + */ + +const secretTextAuth = (secretText: string): SecretTextAuthValue => ({ + authType: AuthType.secretText, + secretText, +}) + +const makeAiKeyProvider = ( + provider: AiKeyProvider, + displayName: string, +): ConnectionProvider => ({ + kind: "integration", + strategy: "api_key", + multiAccount: false, + configFields: [ + { + name: "apiKey", + type: "secret", + required: true, + labelKey: `integrations.${provider}.fields.apiKey`, + }, + ], + // Auth carries only the secret — there is no external account id or name + // to surface, so `sourceId` is the workspace singleton and `displayName` + // is the fixed provider label. + describe: () => ({ sourceId: "workspace", displayName }), + fromCredentials: async ({ apiKey }) => { + if (!(await verifyAiProviderApiKey(provider, apiKey))) { + throw new Error(`Invalid ${displayName} API key`) + } + return secretTextAuth(apiKey) + }, + verify: async ({ auth }) => { + const ok = await verifyAiProviderApiKey(provider, auth.secretText) + return ok + ? { ok: true } + : { ok: false, revoked: true, error: "Invalid API key" } + }, + // Rejection is detected by the same live "list models" probe used for + // `verify`/`fromCredentials` (HTTP 401/403) — there is no separate revoked- + // token error shape for a bare API key, so this always returns `false`; + // `verify` is what surfaces the unhealthy state. + isRevokedTokenError: () => false, +}) + +export const claudeConnectionProvider = makeAiKeyProvider("claude", "Claude") +export const deepseekConnectionProvider = makeAiKeyProvider( + "deepseek", + "DeepSeek", +) +export const geminiConnectionProvider = makeAiKeyProvider("gemini", "Gemini") +export const openaiConnectionProvider = makeAiKeyProvider("openai", "OpenAI") +export const openrouterConnectionProvider = makeAiKeyProvider( + "openrouter", + "OpenRouter", +) + +const OPENAI_COMPATIBLE_VERIFY_TIMEOUT_MS = 10_000 +const TRAILING_SLASH_RE = /\/$/ + +export type OpenaiCompatibleCredentials = { apiKey: string; baseURL: string } + +/** + * OpenAI-compatible presets have no fixed provider host, so verification + * calls the user-supplied `baseURL`'s `/models` endpoint directly (the same + * OpenAI-style contract every compatible provider implements) rather than + * reusing `verifyAiProviderApiKey`'s fixed per-provider URL table. + */ +export const openaiCompatibleConnectionProvider: ConnectionProvider< + SecretTextAuthValue, + OpenaiCompatibleCredentials +> = { + kind: "integration", + strategy: "api_key", + // A workspace may connect more than one preset (`preset <> 'custom'` is + // the only uniqueness constraint) — `CONNECTION_STORE_BINDINGS.openaiCompatible` + // locating a row by bare `workspaceId` is therefore a Phase 0 simplification + // revisited once Phase 2 ports real multi-row disconnect/reconnect matching. + multiAccount: true, + configFields: [ + { + name: "baseURL", + type: "url", + required: true, + labelKey: "integrations.openaiCompatible.fields.baseURL", + }, + { + name: "apiKey", + type: "secret", + required: true, + labelKey: "integrations.openaiCompatible.fields.apiKey", + }, + ], + describe: () => ({ sourceId: "workspace", displayName: "OpenAI-compatible" }), + fromCredentials: async ({ apiKey, baseURL }) => { + const health = await verifyOpenaiCompatibleEndpoint(baseURL, apiKey) + if (!health.ok) { + throw new Error(health.error) + } + return secretTextAuth(apiKey) + }, + verify: ({ auth }) => { + // `auth` alone has no `baseURL` (it lives on the DB row's own column, not + // the auth jsonb) — a real health check needs that row's `baseURL` + // alongside `auth.secretText`, wired in Phase 2 via `ConnectionService`. + // Until then, treat presence of a stored secret as sufficient. + return Promise.resolve( + auth.secretText + ? { ok: true as const } + : { ok: false as const, revoked: true, error: "Missing API key" }, + ) + }, + isRevokedTokenError: () => false, +} + +const verifyOpenaiCompatibleEndpoint = async ( + baseURL: string, + apiKey: string, +): Promise<{ ok: true } | { ok: false; error: string }> => { + try { + await ky.get(`${baseURL.replace(TRAILING_SLASH_RE, "")}/models`, { + headers: { Authorization: `Bearer ${apiKey}` }, + timeout: OPENAI_COMPATIBLE_VERIFY_TIMEOUT_MS, + retry: 0, + }) + return { ok: true } + } catch (error) { + if ( + error instanceof HTTPError && + (error.response.status === 401 || error.response.status === 403) + ) { + return { ok: false, error: "Invalid API key" } + } + // Transient failures don't prove the credential is invalid. + return { ok: true } + } +} diff --git a/packages/business/src/connection/index.ts b/packages/business/src/connection/index.ts new file mode 100644 index 0000000000..5c11082831 --- /dev/null +++ b/packages/business/src/connection/index.ts @@ -0,0 +1,5 @@ +export * from "./adapter" +export * from "./credential-providers" +export * from "./state" +export * from "./state-service" +export * from "./store-bindings" diff --git a/packages/business/src/connection/state-service.ts b/packages/business/src/connection/state-service.ts new file mode 100644 index 0000000000..bc964c0ee5 --- /dev/null +++ b/packages/business/src/connection/state-service.ts @@ -0,0 +1,316 @@ +import { type DatabaseClient, db, eq } from "@chatbotx.io/database/client" +import { + CONNECTION_TO_INBOX_DISCONNECT_REASON, + type ConnectionStatus, + type ConnectionStatusReason, + type IntegrationType, +} from "@chatbotx.io/database/partials" +import { + type ConnectionListInput, + connectionRepository, +} from "@chatbotx.io/database/repositories" +import { inboxModel } from "@chatbotx.io/database/schema" +import type { ConnectionModel } from "@chatbotx.io/database/types" +import { BaseService } from "../base.service" +import { logger } from "../logger" +import { quotaEnforcementService } from "../quota-enforcement/service" +import { workspaceUsageService } from "../workspace-usage/service" +import { type ConnectionEvent, transitionConnection } from "./state" + +class ConnectionNotFoundException extends Error { + constructor(id: string) { + super(`Connection ${id} not found`) + this.name = "ConnectionNotFoundException" + } +} + +/** + * DB-backed reads/writes over the `Connection` table plus its `Inbox` + * legacy-status mirror. Deliberately **registry-free** — it never imports + * `@chatbotx.io/connections` — so it stays safe to call from `markOffline` + * hooks and webhook handlers that must not pull in the full provider + * registry's module graph. The registry-aware orchestration (provider + * `disconnect`/`webhook.unsubscribe` calls, store-binding CRUD) lives in + * `ConnectionService` (Phase 2), which calls this service for the state + * transition itself. + * + * Deferred to Phase 2 (not yet wired here): `Integration.tokenRefreshError`, + * `FacebookAds.status`, `MetaCatalog.status` legacy mirrors, and the + * `dashboardEventBus.emit("connection:changed")` notification — both need + * either registry lookups or new event-schema registration this pass keeps + * out of scope. `Inbox.status`/`disconnectReason` (the mirror the trial-expiry + * banner and every existing channel-status read already depends on) is wired. + */ +class ConnectionStateService extends BaseService { + async list(input: ConnectionListInput) { + const [data, count] = await Promise.all([ + connectionRepository.list(input), + connectionRepository.count(input), + ]) + return { data, count } + } + + async getForWorkspace(input: { + id: string + workspaceId: string + }): Promise { + return await connectionRepository.findByIdForWorkspace(input) + } + + /** `PATCH /v1/connections/{id}` — the only field this route may change; provider config stays on provider-specific routes. */ + async updateDisplayName(input: { + id: string + workspaceId: string + displayName: string + }): Promise { + const existing = await connectionRepository.findByIdForWorkspace({ + id: input.id, + workspaceId: input.workspaceId, + }) + if (!existing) { + return + } + return await connectionRepository.update({ + id: existing.id, + values: { displayName: input.displayName }, + }) + } + + async listDueForRefresh(input: { + before: Date + statuses: ConnectionStatus[] + }): Promise { + return await connectionRepository.listDueForRefresh(input) + } + + /** + * Applies one FSM event to an existing `Connection` row: computes the next + * status via the pure `transitionConnection` (`./state.ts`), writes it, + * mirrors `Inbox.status`/`disconnectReason` when the connection is + * inbox-bound, and — on the state machine's `quotaEdge` — consumes or + * releases exactly one unit of the caller-supplied quota owner's + * `channels` metric. Best-effort: a quota-release failure never rolls back + * the status write (the nightly reconcile self-heals), matching + * `inboxService.disconnect`'s existing behavior. + */ + async transition(input: { + connectionId: string + event: ConnectionEvent + reason?: ConnectionStatusReason + /** Required when the event can consume/release quota (all except read-only transitions). */ + ownerId?: string + tx?: DatabaseClient + }): Promise { + const client = input.tx ?? db + const existing = await connectionRepository.findById( + { id: input.connectionId }, + client, + ) + if (!existing) { + throw new ConnectionNotFoundException(input.connectionId) + } + + const result = transitionConnection({ + from: existing.status, + event: input.event, + reason: input.reason, + }) + + const updated = await connectionRepository.update( + { + id: existing.id, + values: { + status: result.to, + statusReason: result.reason, + connectedAt: + result.quotaEdge === "consume" ? new Date() : existing.connectedAt, + disconnectedAt: + result.quotaEdge === "release" + ? new Date() + : existing.disconnectedAt, + }, + }, + client, + ) + if (!updated) { + throw new ConnectionNotFoundException(input.connectionId) + } + + if (existing.inboxId) { + await this.mirrorInboxStatus({ + inboxId: existing.inboxId, + to: result.to, + reason: result.reason, + tx: client, + }) + } + + // Gated on `kind === "channel"` here (not just `input.ownerId` being + // set) as defense-in-depth: the "channels" quota metric only applies to + // channel connections, so a caller that mistakenly resolves and passes + // an `ownerId` for a workspace-integration/sub-connection row (AI + // providers, marketing tools) still cannot mis-consume/release a + // channel-quota slot. + if (result.quotaEdge && input.ownerId && existing.kind === "channel") { + await this.applyQuotaEdge( + result.quotaEdge, + input.ownerId, + existing.workspaceId, + ) + } + + return updated + } + + /** + * Convenience wrapper over `transition` for the common "provider says the + * token/account is no longer usable" path (`AuthStore.markOffline`, + * webhook-driven revocation) — always fires `auth.revoked`. + */ + async markUnhealthy(input: { + connectionId: string + reason?: ConnectionStatusReason + ownerId?: string + tx?: DatabaseClient + }): Promise { + return await this.transition({ + connectionId: input.connectionId, + event: "auth.revoked", + reason: input.reason ?? "token_revoked", + ownerId: input.ownerId, + tx: input.tx, + }) + } + + /** + * Same as `markUnhealthy`, resolved by `(provider, sourceId)` instead of a + * known `Connection.id` — the shape a provider webhook payload (TikTok + * `authorization.removed`'s `openId`, etc.) actually carries. Silently + * no-ops when no matching connection exists (an orphaned/duplicate webhook + * delivery, not a caller error). + */ + async markUnhealthyByIdentifier(input: { + provider: IntegrationType + identifier: string + reason?: ConnectionStatusReason + ownerId?: string + }): Promise { + const existing = + await connectionRepository.findByProviderAndSourceIdAnyWorkspace({ + provider: input.provider, + sourceId: input.identifier, + }) + if (!existing) { + return null + } + return await this.markUnhealthy({ + connectionId: existing.id, + reason: input.reason, + ownerId: input.ownerId, + }) + } + + /** Records a successful `AuthStore.save` — clears any error, refreshes `authExpiresAt`, transitions back to `connected` if currently `degraded`. */ + async recordAuthSaved(input: { + connectionId: string + authExpiresAt?: Date | null + tx?: DatabaseClient + }): Promise { + const client = input.tx ?? db + await connectionRepository.update( + { + id: input.connectionId, + values: { authExpiresAt: input.authExpiresAt ?? null, lastError: null }, + }, + client, + ) + return await this.transition({ + connectionId: input.connectionId, + event: "auth.saved", + tx: client, + }) + } + + private async mirrorInboxStatus(input: { + inboxId: string + to: ConnectionStatus + reason: ConnectionStatusReason | null + tx: DatabaseClient + }): Promise { + const isActive = input.to === "connected" || input.to === "degraded" + await input.tx + .update(inboxModel) + .set( + isActive + ? { + status: "connected", + disconnectedAt: null, + disconnectReason: null, + } + : { + status: "disconnected", + disconnectedAt: new Date(), + disconnectReason: input.reason + ? CONNECTION_TO_INBOX_DISCONNECT_REASON[input.reason] + : "manual", + }, + ) + .where(eq(inboxModel.id, input.inboxId)) + } + + /** + * Consumes/releases exactly one unit of the owner's `channels` enforcement + * quota, then best-effort mirrors the SAME edge onto the workspace's + * display-only `channels` usage counter — the counterpart to + * `inboxService.create`'s `!skipQuota` branch and `inboxService + * .disconnect`'s unconditional decrement, both of which this method's + * callers (`connectCandidate`/`connectFromCredentials`'s revive path via + * `skipQuota: true`, and disconnect via `mirrorInboxStatus` instead of + * `inboxService.disconnect`) deliberately bypass so this is the only + * place either counter moves for a Connection-domain channel. + */ + private async applyQuotaEdge( + edge: "consume" | "release", + ownerId: string, + workspaceId: string, + ): Promise { + if (edge === "consume") { + const consumed = await quotaEnforcementService.tryConsume({ + userId: ownerId, + metric: "channels", + }) + if (!consumed.ok) { + // The caller already committed the status write; a failed consume + // here means the FSM and the quota ledger disagree. Surface it + // rather than silently under-counting — the nightly reconcile + // (Phase 4) is the long-term fix for this race. + throw new Error( + `connection quota consume failed for owner ${ownerId} after status transition committed`, + ) + } + await workspaceUsageService + .increment(workspaceId, "channels") + .catch((err) => { + logger.warn( + { err, workspaceId, ownerId }, + "connection connect: workspace usage channel increment failed", + ) + }) + return + } + await quotaEnforcementService.release({ + userId: ownerId, + metric: "channels", + }) + await workspaceUsageService + .decrement(workspaceId, "channels") + .catch((err) => { + logger.warn( + { err, workspaceId, ownerId }, + "connection disconnect: workspace usage channel decrement failed", + ) + }) + } +} + +export const connectionStateService = new ConnectionStateService() diff --git a/packages/business/src/connection/state.ts b/packages/business/src/connection/state.ts new file mode 100644 index 0000000000..22ed352c90 --- /dev/null +++ b/packages/business/src/connection/state.ts @@ -0,0 +1,162 @@ +/** + * Pure Connection status state machine — zero imports beyond the enum types, + * so it can be unit-tested without a database and reused by both + * `ConnectionStateService.transition` (DB writes + audit) and any future + * dry-run/preview caller. + * + * Families: ACTIVE = `connected | degraded` (quota held, `Inbox.status = + * connected`). INACTIVE = `needs_reauth | paused | disconnected` (quota + * released, `Inbox.status = disconnected`). + * + * Invariant: quota is consumed exactly on an INACTIVE/absent → ACTIVE edge + * and released exactly on ACTIVE → INACTIVE, nowhere else. `quotaEdge` below + * is the single source of truth callers use to decide whether to + * consume/release quota — never re-derive it ad hoc. + */ +import { + ACTIVE_CONNECTION_STATUSES, + type ConnectionStatus, + type ConnectionStatusReason, +} from "@chatbotx.io/database/partials" + +export type ConnectionEvent = + | "connect.completed" + | "auth.saved" + | "refresh.transient_failure" + | "verify.failed_non_auth" + | "verify.ok" + | "auth.revoked" + | "user.disconnect" + | "teardown.pause" + | "teardown.resume" + | "teardown.disconnect" + +export type ConnectionTransitionInput = { + /** `undefined` when no `Connection` row exists yet (first `connect.completed`). */ + from: ConnectionStatus | undefined + event: ConnectionEvent + reason?: ConnectionStatusReason +} + +export type ConnectionTransitionResult = { + to: ConnectionStatus + reason: ConnectionStatusReason | null + /** `null` = no quota change; `"consume"`/`"release"` = the edge callers must act on exactly once. */ + quotaEdge: "consume" | "release" | null + /** Idempotent no-op: `to === from`, event carried no real state change. */ + noop: boolean +} + +export const isActiveConnectionStatus = (status: ConnectionStatus): boolean => + (ACTIVE_CONNECTION_STATUSES as readonly ConnectionStatus[]).includes(status) + +class InvalidConnectionTransitionException extends Error { + constructor(from: ConnectionStatus | undefined, event: ConnectionEvent) { + super( + `Connection cannot handle event "${event}" from status "${from ?? "∅"}"`, + ) + this.name = "InvalidConnectionTransitionException" + } +} + +const quotaEdgeFor = ( + from: ConnectionStatus | undefined, + to: ConnectionStatus, +): "consume" | "release" | null => { + const wasActive = from !== undefined && isActiveConnectionStatus(from) + const isActive = isActiveConnectionStatus(to) + if (!wasActive && isActive) { + return "consume" + } + if (wasActive && !isActive) { + return "release" + } + return null +} + +const result = ( + from: ConnectionStatus | undefined, + to: ConnectionStatus, + reason: ConnectionStatusReason | null, +): ConnectionTransitionResult => ({ + to, + reason, + quotaEdge: quotaEdgeFor(from, to), + noop: from === to, +}) + +/** + * Resolve one event against the current status. Throws + * {@link InvalidConnectionTransitionException} only for `refresh`/`verify` + * events fired against an INACTIVE status (409 `CONNECTION_INACTIVE` at the + * API layer) — every other unmodeled combination is an idempotent no-op that + * returns the current status unchanged. + */ +export const transitionConnection = ( + input: ConnectionTransitionInput, +): ConnectionTransitionResult => { + const { from, event, reason } = input + + switch (event) { + case "connect.completed": { + if ( + from !== undefined && + from !== "disconnected" && + from !== "needs_reauth" + ) { + return result(from, from, null) + } + return result(from, "connected", null) + } + case "auth.saved": { + if (from === undefined || !isActiveConnectionStatus(from)) { + throw new InvalidConnectionTransitionException(from, event) + } + return result(from, "connected", null) + } + case "refresh.transient_failure": + case "verify.failed_non_auth": { + if (from === undefined || !isActiveConnectionStatus(from)) { + throw new InvalidConnectionTransitionException(from, event) + } + return result(from, "degraded", reason ?? "refresh_failed") + } + case "verify.ok": { + if (from === undefined || !isActiveConnectionStatus(from)) { + throw new InvalidConnectionTransitionException(from, event) + } + return result(from, "connected", null) + } + case "auth.revoked": { + if (from === undefined || !isActiveConnectionStatus(from)) { + return result(from, from ?? "needs_reauth", null) + } + return result(from, "needs_reauth", reason ?? "token_revoked") + } + case "user.disconnect": { + if (from === undefined) { + return result(from, "disconnected", "manual") + } + return result(from, "disconnected", reason ?? "manual") + } + case "teardown.pause": { + if (from === undefined || !isActiveConnectionStatus(from)) { + return result(from, from ?? "paused", null) + } + return result(from, "paused", reason ?? "trial_expired") + } + case "teardown.resume": { + if (from !== "paused") { + return result(from, from ?? "paused", null) + } + return result(from, "connected", null) + } + case "teardown.disconnect": { + return result(from, "disconnected", reason ?? "workspace_purge") + } + default: { + const _exhaustive: never = event + throw new InvalidConnectionTransitionException(from, _exhaustive) + } + } +} diff --git a/packages/business/src/connection/store-bindings.ts b/packages/business/src/connection/store-bindings.ts new file mode 100644 index 0000000000..09543f968f --- /dev/null +++ b/packages/business/src/connection/store-bindings.ts @@ -0,0 +1,547 @@ +import type { DatabaseClient } from "@chatbotx.io/database/client" +import { db, eq } from "@chatbotx.io/database/client" +import type { IntegrationType } from "@chatbotx.io/database/partials" +import { + integrationActiveCampaignModel, + integrationApiModel, + integrationClaudeModel, + integrationDeepseekModel, + integrationDripModel, + integrationFacebookAdsModel, + integrationGeminiModel, + integrationGetResponseModel, + integrationGoogleCalendarModel, + integrationGoogleSheetsModel, + integrationInstagramModel, + integrationKlaviyoModel, + integrationMailchimpModel, + integrationMailerLiteModel, + integrationMessengerModel, + integrationMetaCatalogModel, + integrationModel, + integrationMoosendModel, + integrationOpenaiCompatibleModel, + integrationOpenaiModel, + integrationOpenrouterModel, + integrationOutlookCalendarModel, + integrationSendGridModel, + integrationSmtpModel, + integrationTelegramModel, + integrationTiktokModel, + integrationWebchatModel, + integrationWhatsappModel, + integrationZaloModel, +} from "@chatbotx.io/database/schema" +import type { AuthValue } from "@chatbotx.io/sdk" +import type { AnyPgColumn, PgTable } from "drizzle-orm/pg-core" + +/** Minimal shape a binding's DB row is normalized to. */ +export type ConnectionStoreRow = { + id: string + inboxId?: string | null + integrationId?: string | null + auth: AuthValue +} + +export type ConnectionStoreInsertInput = { + workspaceId: string + inboxId?: string + integrationId?: string + auth: AuthValue + descriptor: { sourceId: string; displayName: string } + config?: Record +} + +/** + * Per-`IntegrationType` DB adapter the Connection domain drives instead of + * each channel/integration hand-rolling insert/delete/lookup logic. `table` + * is the SQL table name (matches `Connection.provider`'s satellite row); + * `identityColumn` is the column that carries the provider's natural + * external id (`pageId`, `phoneNumberId`, `botId`, …) — `null` when the row + * has no external identity beyond its own `id` (api/smtp/webchat). + */ +export type ConnectionStoreBinding = { + table: string + identityColumn: string | null + loadAuth: (rowId: string, tx?: DatabaseClient) => Promise + /** + * Same read as `loadAuth`, keyed by the FK the `Connection` row actually + * carries (`inboxId` for channels, `integrationId` for workspace + * integrations) — `Connection` has no column pointing at the satellite + * row's own primary key, so this is what `ConnectionService` (disconnect/ + * refresh/verify) uses to reach the row. + */ + loadAuthByForeignKey: ( + foreignKey: string, + tx?: DatabaseClient, + ) => Promise + /** Persists a refreshed auth value by the same FK as {@link loadAuthByForeignKey} — used by `ConnectionService.refresh`'s `AuthStore.save`. */ + saveAuthByForeignKey: ( + foreignKey: string, + auth: AuthValue, + tx?: DatabaseClient, + ) => Promise + /** + * `id` is always the satellite row's own PK. `integrationId` is also + * populated for a workspace-integration binding (the parent `Integration` + * row `insertRow` creates in the same transaction) — `Connection` has no + * column pointing at a satellite's own PK, so `ConnectionService` needs + * this to set `Connection.integrationId`. `undefined` for a channel + * binding, whose FK is the caller-supplied `inboxId` it already has. + */ + insertRow: ( + input: ConnectionStoreInsertInput, + tx?: DatabaseClient, + ) => Promise<{ id: string; integrationId?: string }> + /** `delete_row`: messenger/instagram-style disconnect deletes the satellite row. `keep_row`: whatsapp/zalo/… keep it for reconnect matching (today's behaviour). */ + onDisconnect: "delete_row" | "keep_row" + /** No-ops when `onDisconnect === "keep_row"`. Same FK as `loadAuthByForeignKey`. */ + deleteRowByForeignKey: ( + foreignKey: string, + tx?: DatabaseClient, + ) => Promise + findRowByIdentifier: ( + identifier: string, + tx?: DatabaseClient, + ) => Promise + /** Unique-constraint name a duplicate insert violates — lets callers map it to `alreadyConnected` instead of a raw DB error. */ + duplicateConstraint?: string +} + +/** + * `AnyPgColumn`'s data type is erased to `unknown` by design (it spans every + * column type in the schema). Every generic binding factory below reads a + * jsonb `auth`/`encryptedAuth` column through this type, so the cast to + * `AuthValue` happens once here rather than at each call site — Drizzle has + * no way to express "this specific dynamic column is jsonb shaped like + * AuthValue" generically. + */ +const asAuthValue = (value: unknown): AuthValue => value as AuthValue + +/** + * Channel-satellite binding: a table with `id`, `workspaceId`, `inboxId`, + * `auth`, `name`, and (usually) one natural external identity column. Covers + * api/messenger/whatsapp/zalo/smtp/telegram/tiktok/webchat and the + * instagram/instagramFacebook split over `IntegrationInstagram`. + */ +type ChannelSatelliteTable = PgTable & { + id: AnyPgColumn + auth: AnyPgColumn + name: AnyPgColumn + workspaceId: AnyPgColumn + inboxId: AnyPgColumn +} + +const makeChannelBinding = (opts: { + table: TTable + tableName: string + identityColumn: (Extract & string) | null + onDisconnect: "delete_row" | "keep_row" + duplicateConstraint?: string + /** Extra fixed columns to set on insert (e.g. `IntegrationInstagram.type`). */ + extraInsertValues?: Record + /** Extra equality narrowing every read must apply (e.g. `type = 'instagram'`). */ + extraWhere?: Record +}): ConnectionStoreBinding => { + const { table } = opts + // `.from()`/`.insert()` reject a generic `TTable` param (Drizzle's typing + // resolves them against the exact table's config, which a shared factory + // spanning 15 distinct tables cannot express) — upcast once here; column + // references below stay on the narrowed `table` for real type checking. + const rawTable: PgTable = table + // `identityColumn`, when set, is asserted (Phase 0 registration) to name a + // real text column on `table`; there is no generic way to encode "this + // string literal names a column of this specific table" across 15 distinct + // table shapes, so the lookup is a single documented cast. + const identityCol = opts.identityColumn + ? (table[opts.identityColumn as keyof TTable] as unknown as AnyPgColumn) + : null + + return { + table: opts.tableName, + identityColumn: opts.identityColumn, + loadAuth: async (rowId, tx = db) => { + const [row] = await tx + .select({ auth: table.auth }) + .from(rawTable) + .where(eq(table.id, rowId)) + .limit(1) + if (!row) { + throw new Error(`Unable to load auth for ${opts.tableName} ${rowId}`) + } + return asAuthValue(row.auth) + }, + loadAuthByForeignKey: async (inboxId, tx = db) => { + const [row] = await tx + .select({ auth: table.auth }) + .from(rawTable) + .where(eq(table.inboxId, inboxId)) + .limit(1) + if (!row) { + throw new Error( + `Unable to load auth for ${opts.tableName} inbox ${inboxId}`, + ) + } + return asAuthValue(row.auth) + }, + saveAuthByForeignKey: async (inboxId, auth, tx = db) => { + await tx + .update(rawTable) + .set({ auth } as never) + .where(eq(table.inboxId, inboxId)) + }, + insertRow: async (input, tx = db) => { + const identityValues = identityCol + ? { [opts.identityColumn as string]: input.descriptor.sourceId } + : {} + const values = { + workspaceId: input.workspaceId, + inboxId: input.inboxId, + auth: input.auth, + name: input.descriptor.displayName, + ...identityValues, + ...opts.extraInsertValues, + ...input.config, + } + // Each channel table adds its own extra required/defaulted columns + // beyond this shared shape (e.g. `IntegrationApi.tokenHash`), so the + // generic factory cannot express the exact per-table insert type. + const [row] = await tx + .insert(rawTable) + .values(values as never) + .returning({ id: table.id }) + return { id: row.id as string } + }, + onDisconnect: opts.onDisconnect, + deleteRowByForeignKey: async (inboxId, tx = db) => { + if (opts.onDisconnect === "keep_row") { + return + } + await tx.delete(rawTable).where(eq(table.inboxId, inboxId)) + }, + findRowByIdentifier: async (identifier, tx = db) => { + if (!identityCol) { + return null + } + const [row] = await tx + .select({ id: table.id, inboxId: table.inboxId, auth: table.auth }) + .from(rawTable) + .where(eq(identityCol, identifier)) + .limit(1) + if (!row) { + return null + } + return { + id: row.id as string, + inboxId: row.inboxId as string, + auth: asAuthValue(row.auth), + } + }, + duplicateConstraint: opts.duplicateConstraint, + } +} + +/** + * Workspace-level satellite binding: `insertRow` creates the parent + * `Integration` row (`workspaceId`, `integrationType`) and the satellite row + * in one transaction, mirroring today's per-provider connect actions. These + * are all singletons (`sourceId = "workspace"`); `identityColumn` is `null` + * because the natural key is `workspaceId`, not a column on the satellite. + */ +type WorkspaceSatelliteTable = PgTable & { + id: AnyPgColumn + workspaceId: AnyPgColumn + integrationId: AnyPgColumn +} + +const makeWorkspaceIntegrationBinding = < + TTable extends WorkspaceSatelliteTable, +>(opts: { + table: TTable + tableName: string + integrationType: IntegrationType + authColumn?: "auth" | "encryptedAuth" + duplicateConstraint?: string +}): ConnectionStoreBinding => { + const { table } = opts + // `.from()`/`.insert()` reject a generic `TTable` param — see the same + // note in `makeChannelBinding` above. + const rawTable: PgTable = table + const authColumnName = opts.authColumn ?? "auth" + // Only `IntegrationMetaCatalog` names its auth column `encryptedAuth`; + // every other satellite uses `auth`. The generic factory resolves whichever + // one this table actually has via a single documented cast. + const authColumn = table[ + authColumnName as keyof TTable + ] as unknown as AnyPgColumn + + return { + table: opts.tableName, + identityColumn: null, + loadAuth: async (rowId, tx = db) => { + const [row] = await tx + .select({ auth: authColumn }) + .from(rawTable) + .where(eq(table.id, rowId)) + .limit(1) + if (!row) { + throw new Error(`Unable to load auth for ${opts.tableName} ${rowId}`) + } + return asAuthValue(row.auth) + }, + loadAuthByForeignKey: async (integrationId, tx = db) => { + const [row] = await tx + .select({ auth: authColumn }) + .from(rawTable) + .where(eq(table.integrationId, integrationId)) + .limit(1) + if (!row) { + throw new Error( + `Unable to load auth for ${opts.tableName} integration ${integrationId}`, + ) + } + return asAuthValue(row.auth) + }, + saveAuthByForeignKey: async (integrationId, auth, tx = db) => { + await tx + .update(rawTable) + .set({ [authColumnName]: auth } as never) + .where(eq(table.integrationId, integrationId)) + }, + insertRow: async (input, tx) => { + const run = async (client: DatabaseClient) => { + const [parent] = await client + .insert(integrationModel) + .values({ + workspaceId: input.workspaceId, + integrationType: opts.integrationType, + }) + .returning({ id: integrationModel.id }) + const values = { + workspaceId: input.workspaceId, + integrationId: parent.id, + [authColumnName]: input.auth, + ...input.config, + } + // Same per-table shape gap as `makeChannelBinding.insertRow` above. + const [row] = await client + .insert(rawTable) + .values(values as never) + .returning({ id: table.id }) + return { id: row.id as string, integrationId: parent.id as string } + } + return tx ? await run(tx) : await db.transaction((trx) => run(trx)) + }, + onDisconnect: "delete_row", + deleteRowByForeignKey: async (integrationId, tx = db) => { + await tx.delete(rawTable).where(eq(table.integrationId, integrationId)) + }, + findRowByIdentifier: async (identifier, tx = db) => { + // Workspace singletons are located by `workspaceId`, passed as `identifier`. + const [row] = await tx + .select({ + id: table.id, + integrationId: table.integrationId, + auth: authColumn, + }) + .from(rawTable) + .where(eq(table.workspaceId, identifier)) + .limit(1) + if (!row) { + return null + } + return { + id: row.id as string, + integrationId: row.integrationId as string, + auth: asAuthValue(row.auth), + } + }, + duplicateConstraint: opts.duplicateConstraint, + } +} + +export const CONNECTION_STORE_BINDINGS: Record< + IntegrationType, + ConnectionStoreBinding | null +> = { + activeCampaign: makeWorkspaceIntegrationBinding({ + table: integrationActiveCampaignModel, + tableName: "IntegrationActiveCampaign", + integrationType: "activeCampaign", + duplicateConstraint: "IntegrationActiveCampaign_workspaceId_key", + }), + api: makeChannelBinding({ + table: integrationApiModel, + tableName: "IntegrationApi", + identityColumn: null, + onDisconnect: "keep_row", + }), + chatbotx: null, + claude: makeWorkspaceIntegrationBinding({ + table: integrationClaudeModel, + tableName: "IntegrationClaude", + integrationType: "claude", + duplicateConstraint: "IntegrationClaude_workspaceId_key", + }), + deepseek: makeWorkspaceIntegrationBinding({ + table: integrationDeepseekModel, + tableName: "IntegrationDeepseek", + integrationType: "deepseek", + duplicateConstraint: "IntegrationDeepseek_workspaceId_key", + }), + drip: makeWorkspaceIntegrationBinding({ + table: integrationDripModel, + tableName: "IntegrationDrip", + integrationType: "drip", + duplicateConstraint: "IntegrationDrip_workspaceId_key", + }), + facebookAds: makeWorkspaceIntegrationBinding({ + table: integrationFacebookAdsModel, + tableName: "IntegrationFacebookAds", + integrationType: "facebookAds", + duplicateConstraint: "IntegrationFacebookAds_workspaceId_key", + }), + gemini: makeWorkspaceIntegrationBinding({ + table: integrationGeminiModel, + tableName: "IntegrationGemini", + integrationType: "gemini", + duplicateConstraint: "IntegrationGemini_workspaceId_key", + }), + getResponse: makeWorkspaceIntegrationBinding({ + table: integrationGetResponseModel, + tableName: "IntegrationGetResponse", + integrationType: "getResponse", + duplicateConstraint: "IntegrationGetResponse_workspaceId_key", + }), + googleCalendar: makeWorkspaceIntegrationBinding({ + table: integrationGoogleCalendarModel, + tableName: "IntegrationGoogleCalendar", + integrationType: "googleCalendar", + }), + googleSheets: makeWorkspaceIntegrationBinding({ + table: integrationGoogleSheetsModel, + tableName: "IntegrationGoogleSheet", + integrationType: "googleSheets", + }), + instagram: makeChannelBinding({ + table: integrationInstagramModel, + tableName: "IntegrationInstagram", + identityColumn: "igId", + onDisconnect: "delete_row", + duplicateConstraint: "IntegrationInstagram_igId_key", + extraInsertValues: { type: "instagram" }, + extraWhere: { type: "instagram" }, + }), + instagramFacebook: makeChannelBinding({ + table: integrationInstagramModel, + tableName: "IntegrationInstagram", + identityColumn: "igId", + onDisconnect: "delete_row", + duplicateConstraint: "IntegrationInstagram_igId_key", + extraInsertValues: { type: "facebook" }, + extraWhere: { type: "facebook" }, + }), + klaviyo: makeWorkspaceIntegrationBinding({ + table: integrationKlaviyoModel, + tableName: "IntegrationKlaviyo", + integrationType: "klaviyo", + duplicateConstraint: "IntegrationKlaviyo_workspaceId_key", + }), + mailchimp: makeWorkspaceIntegrationBinding({ + table: integrationMailchimpModel, + tableName: "IntegrationMailchimp", + integrationType: "mailchimp", + }), + mailerLite: makeWorkspaceIntegrationBinding({ + table: integrationMailerLiteModel, + tableName: "IntegrationMailerLite", + integrationType: "mailerLite", + duplicateConstraint: "IntegrationMailerLite_workspaceId_key", + }), + messenger: makeChannelBinding({ + table: integrationMessengerModel, + tableName: "IntegrationMessenger", + identityColumn: "pageId", + onDisconnect: "delete_row", + duplicateConstraint: "IntegrationMessenger_pageId_key", + }), + metaCatalog: makeWorkspaceIntegrationBinding({ + table: integrationMetaCatalogModel, + tableName: "IntegrationMetaCatalog", + integrationType: "metaCatalog", + authColumn: "encryptedAuth", + duplicateConstraint: "IntegrationMetaCatalog_workspaceId_key", + }), + moosend: makeWorkspaceIntegrationBinding({ + table: integrationMoosendModel, + tableName: "IntegrationMoosend", + integrationType: "moosend", + duplicateConstraint: "IntegrationMoosend_workspaceId_key", + }), + openai: makeWorkspaceIntegrationBinding({ + table: integrationOpenaiModel, + tableName: "IntegrationOpenai", + integrationType: "openai", + }), + openaiCompatible: makeWorkspaceIntegrationBinding({ + table: integrationOpenaiCompatibleModel, + tableName: "IntegrationOpenaiCompatible", + integrationType: "openaiCompatible", + }), + openrouter: makeWorkspaceIntegrationBinding({ + table: integrationOpenrouterModel, + tableName: "IntegrationOpenrouter", + integrationType: "openrouter", + duplicateConstraint: "IntegrationOpenrouter_workspaceId_key", + }), + outlookCalendar: makeWorkspaceIntegrationBinding({ + table: integrationOutlookCalendarModel, + tableName: "IntegrationOutlookCalendar", + integrationType: "outlookCalendar", + }), + sendGrid: makeWorkspaceIntegrationBinding({ + table: integrationSendGridModel, + tableName: "IntegrationSendGrid", + integrationType: "sendGrid", + duplicateConstraint: "IntegrationSendGrid_workspaceId_key", + }), + smtp: makeChannelBinding({ + table: integrationSmtpModel, + tableName: "IntegrationSmtp", + identityColumn: null, + onDisconnect: "keep_row", + }), + telegram: makeChannelBinding({ + table: integrationTelegramModel, + tableName: "IntegrationTelegram", + identityColumn: "botId", + onDisconnect: "keep_row", + duplicateConstraint: "IntegrationTelegram_botId_key", + }), + tiktok: makeChannelBinding({ + table: integrationTiktokModel, + tableName: "IntegrationTiktok", + identityColumn: "openId", + onDisconnect: "keep_row", + duplicateConstraint: "IntegrationTiktok_openId_key", + }), + webchat: makeChannelBinding({ + table: integrationWebchatModel, + tableName: "IntegrationWebchat", + identityColumn: null, + onDisconnect: "keep_row", + }), + whatsapp: makeChannelBinding({ + table: integrationWhatsappModel, + tableName: "IntegrationWhatsapp", + identityColumn: "phoneNumberId", + onDisconnect: "keep_row", + duplicateConstraint: "IntegrationWhatsapp_phoneNumberId_key", + }), + zalo: makeChannelBinding({ + table: integrationZaloModel, + tableName: "IntegrationZalo", + identityColumn: "oaId", + onDisconnect: "keep_row", + }), +} diff --git a/packages/business/src/errors.ts b/packages/business/src/errors.ts index 85be7c1a04..ed0b0198cc 100644 --- a/packages/business/src/errors.ts +++ b/packages/business/src/errors.ts @@ -256,3 +256,90 @@ export const workspaceLimitReachedException = () => "Workspace limit reached for this plan", "workspaceLimitReached", ) + +/** + * `refresh`/`verify` fired against a `Connection` whose status is not one of + * `ACTIVE_CONNECTION_STATUSES` (`connected`/`degraded`) — matches the FSM's + * own precondition in `transitionConnection` (`state.ts`). + */ +export const connectionInactiveException = () => + new ChatbotXException( + "This connection is not active.", + "connectionInactive", + 409, + ) + +/** No `ConnectionAdapter` (or no store binding on it) is registered for this provider — a registry/data integrity gap, not a user error. */ +export const connectionNotConfiguredException = (provider: string) => + new ChatbotXException( + `Connection provider "${provider}" is not configured.`, + "connectionNotConfigured", + 500, + ) + +export const connectionNotRefreshableException = (provider: string) => + new ChatbotXException( + `Connection provider "${provider}" does not support refresh.`, + "connectionNotRefreshable", + 400, + ) + +/** This provider is already connected in this workspace — a fresh `connectFromCredentials`/OAuth-connect call would collide with the unique `(workspaceId, provider, sourceId)` key. */ +export const connectionAlreadyConnectedException = () => + new ChatbotXException( + "This provider is already connected in this workspace.", + "connectionAlreadyConnected", + 409, + ) + +/** `connectFromCredentials` called against a provider whose `strategy` isn't `token`/`api_key`/`self_serve` (or one that never declared `fromCredentials`). */ +export const connectionWrongStrategyException = (provider: string) => + new ChatbotXException( + `Connection provider "${provider}" does not accept direct credentials.`, + "connectionWrongStrategy", + 400, + ) + +/** `provider.fromCredentials` rejected the supplied config with a live validation call (e.g. an invalid API key). */ +export const connectionCredentialsRejectedException = (message: string) => + new ChatbotXException(message, "connectionCredentialsRejected", 400) + +/** `startSession`/`completeAuthorization` called against a provider with no `authorizeUrl`/`exchangeCode` handler — not an OAuth-strategy provider. */ +export const connectionNotOAuthException = (provider: string) => + new ChatbotXException( + `Connection provider "${provider}" does not support an OAuth connect flow.`, + "connectionNotOAuth", + 400, + ) + +/** The OAuth callback's `sessionId`/nonce did not resolve to a matching `ConnectSession` — a forged, stale, or already-consumed `state` parameter. */ +export const connectionStateMismatchException = () => + new ChatbotXException( + "This connect session could not be verified. Please start the connection again.", + "connectionStateMismatch", + 400, + ) + +/** `listCandidates`/`describe` returned nothing selectable after a successful code exchange. */ +export const connectionNoCandidatesException = () => + new ChatbotXException( + "No connectable accounts were found for this authorization.", + "connectionNoCandidates", + 400, + ) + +/** `reconnect`'s completion: the re-authorized account does not match the `Connection` being reconnected (user picked/granted the wrong account). */ +export const connectionIdentityMismatchException = () => + new ChatbotXException( + "The reauthorized account does not match the connection being reconnected.", + "connectionIdentityMismatch", + 400, + ) + +/** API-driven `POST /v1/connections` for a channel provider hidden by the tenant's channel-visibility policy (not already connected in this workspace) — a deliberate tightening for an unattended caller; the interactive builder UI grandfathers an already-connected channel instead of hiding it. */ +export const channelHiddenException = (channel: string) => + new ChatbotXException( + `The "${channel}" channel is not available for this workspace.`, + "channelHidden", + 403, + ) diff --git a/packages/business/src/inbox/service.ts b/packages/business/src/inbox/service.ts index 004b742509..929d163e7d 100644 --- a/packages/business/src/inbox/service.ts +++ b/packages/business/src/inbox/service.ts @@ -291,12 +291,22 @@ class InboxService extends BaseService { return inboxes.map((inbox) => inbox.id) } + /** + * `skipQuota` bypasses this method's own `tryConsume`/`increment` for a + * brand-new inbox — used only by `ConnectionService.connectTargets` + * (`@chatbotx.io/connections`), which activates the Connection through + * `connectionStateService.transition` right after this call: `transition` + * owns the quota edge for that path, so consuming here too would charge + * the workspace twice for one new channel. Every other caller leaves it + * `false` (default) and keeps today's behaviour unchanged. + */ async create(props: { data: Omit & { id?: string } ownerId: string tx?: DatabaseClient + skipQuota?: boolean }): Promise<{ inbox: InboxModel; wasCreated: boolean }> { - const { data, ownerId, tx = db } = props + const { data, ownerId, tx = db, skipQuota = false } = props const existing = await tx.query.inboxModel.findFirst({ where: { @@ -323,12 +333,14 @@ class InboxService extends BaseService { return { inbox: existing, wasCreated: false } } - const consumed = await quotaEnforcementService.tryConsume({ - userId: ownerId, - metric: "channels", - }) - if (!consumed.ok) { - throw channelLimitReachedException() + if (!skipQuota) { + const consumed = await quotaEnforcementService.tryConsume({ + userId: ownerId, + metric: "channels", + }) + if (!consumed.ok) { + throw channelLimitReachedException() + } } const [inbox] = await tx @@ -336,14 +348,16 @@ class InboxService extends BaseService { .values({ id: data.id ?? createId(), ...data }) .returning() - await workspaceUsageService - .increment(data.workspaceId, "channels") - .catch((err) => { - logger.warn( - { err, workspaceId: data.workspaceId }, - "workspace usage channel increment failed", - ) - }) + if (!skipQuota) { + await workspaceUsageService + .increment(data.workspaceId, "channels") + .catch((err) => { + logger.warn( + { err, workspaceId: data.workspaceId }, + "workspace usage channel increment failed", + ) + }) + } return { inbox, wasCreated: true } } diff --git a/packages/business/src/index.ts b/packages/business/src/index.ts index 2c6c229c60..af45824c5e 100644 --- a/packages/business/src/index.ts +++ b/packages/business/src/index.ts @@ -15,6 +15,8 @@ export * from "./bot-field" export * from "./broadcast" export * from "./coexist" export * from "./coexist-import" +export * from "./connect-session" +export * from "./connection" export * from "./contact" // Exported here, NOT from ./contact/index.ts, on purpose: it drags in // coexist-import + workspace-usage (→ analytics/mac-tracking → redis bloomFilter), diff --git a/apps/builder/__tests__/integration-ai-verify-api-key.test.ts b/packages/business/src/integration-ai-provider/__tests__/verify.test.ts similarity index 72% rename from apps/builder/__tests__/integration-ai-verify-api-key.test.ts rename to packages/business/src/integration-ai-provider/__tests__/verify.test.ts index 86765188b6..965ee023bf 100644 --- a/apps/builder/__tests__/integration-ai-verify-api-key.test.ts +++ b/packages/business/src/integration-ai-provider/__tests__/verify.test.ts @@ -23,10 +23,7 @@ vi.mock("ky", () => ({ HTTPError: MockHTTPError, })) -const { aiProviders } = await import("@chatbotx.io/ai") -const { verifyAiProviderApiKey } = await import( - "@/features/integration-ai/lib/verify-api-key" -) +const { verifyAiProviderApiKey } = await import("../verify") describe("verifyAiProviderApiKey", () => { beforeEach(() => { @@ -36,9 +33,9 @@ describe("verifyAiProviderApiKey", () => { test("checks OpenRouter credentials against the authenticated key endpoint", async () => { mockKyGet.mockResolvedValueOnce({}) - await expect( - verifyAiProviderApiKey(aiProviders.enum.openrouter, "or-key"), - ).resolves.toBe(true) + await expect(verifyAiProviderApiKey("openrouter", "or-key")).resolves.toBe( + true, + ) expect(mockKyGet).toHaveBeenCalledWith( "https://openrouter.ai/api/v1/key", @@ -51,16 +48,16 @@ describe("verifyAiProviderApiKey", () => { test("rejects explicitly unauthorized provider responses", async () => { mockKyGet.mockRejectedValueOnce(new MockHTTPError(401)) - await expect( - verifyAiProviderApiKey(aiProviders.enum.openrouter, "bad-key"), - ).resolves.toBe(false) + await expect(verifyAiProviderApiKey("openrouter", "bad-key")).resolves.toBe( + false, + ) }) test("does not block users on transient provider failures", async () => { mockKyGet.mockRejectedValueOnce(new Error("network unavailable")) await expect( - verifyAiProviderApiKey(aiProviders.enum.openrouter, "possibly-valid-key"), + verifyAiProviderApiKey("openrouter", "possibly-valid-key"), ).resolves.toBe(true) }) }) diff --git a/apps/builder/src/features/integration-ai/lib/verify-api-key.ts b/packages/business/src/integration-ai-provider/verify.ts similarity index 75% rename from apps/builder/src/features/integration-ai/lib/verify-api-key.ts rename to packages/business/src/integration-ai-provider/verify.ts index cdb297e880..92690fea17 100644 --- a/apps/builder/src/features/integration-ai/lib/verify-api-key.ts +++ b/packages/business/src/integration-ai-provider/verify.ts @@ -1,40 +1,53 @@ -import { type AIProvider, aiProviders } from "@chatbotx.io/ai" import ky, { HTTPError } from "ky" const VERIFY_TIMEOUT_MS = 10_000 const UNAUTHORIZED_STATUSES = new Set([401, 403]) +/** + * The subset of `IntegrationType` this module can validate a bare API key + * for. Deliberately a local literal union (not `AIProvider` from + * `@chatbotx.io/ai`) — that package depends on `@chatbotx.io/business`, so + * importing it back here would be circular. Every value here is also a real + * `IntegrationType`. + */ +export type AiKeyProvider = + | "claude" + | "deepseek" + | "gemini" + | "openai" + | "openrouter" + type VerifyConfig = { url: (apiKey: string) => string headers?: (apiKey: string) => Record } // Lightweight "list models" probes used purely to validate an API key. -const verifyConfigByProvider: Partial> = { - [aiProviders.enum.claude]: { +const verifyConfigByProvider: Record = { + claude: { url: () => "https://api.anthropic.com/v1/models", headers: (apiKey) => ({ "x-api-key": apiKey, "anthropic-version": "2023-06-01", }), }, - [aiProviders.enum.deepseek]: { + deepseek: { url: () => "https://api.deepseek.com/models", headers: (apiKey) => ({ Authorization: `Bearer ${apiKey}`, }), }, - [aiProviders.enum.gemini]: { + gemini: { url: (apiKey) => `https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`, }, - [aiProviders.enum.openai]: { + openai: { url: () => "https://api.openai.com/v1/models", headers: (apiKey) => ({ Authorization: `Bearer ${apiKey}`, }), }, - [aiProviders.enum.openrouter]: { + openrouter: { url: () => "https://openrouter.ai/api/v1/key", headers: (apiKey) => ({ Authorization: `Bearer ${apiKey}`, @@ -51,13 +64,10 @@ const verifyConfigByProvider: Partial> = { * blocking the user on an unrelated outage would be a false negative. */ export async function verifyAiProviderApiKey( - provider: AIProvider, + provider: AiKeyProvider, apiKey: string, ): Promise { const config = verifyConfigByProvider[provider] - if (!config) { - return true - } try { await ky.get(config.url(apiKey), { diff --git a/packages/business/src/integration-context/__tests__/auth-store.test.ts b/packages/business/src/integration-context/__tests__/auth-store.test.ts new file mode 100644 index 0000000000..595756623e --- /dev/null +++ b/packages/business/src/integration-context/__tests__/auth-store.test.ts @@ -0,0 +1,229 @@ +import { beforeEach, describe, expect, it, vi } from "vitest" + +const mocks = vi.hoisted(() => ({ + execute: vi.fn(async () => ({ rows: [{ auth: { authType: "none" } }] })), + runExclusive: vi.fn((input: { fn: () => unknown }) => input.fn()), + findByInboxId: vi.fn(), + findByIntegrationId: vi.fn(), + inboxUpdate: vi.fn(), + inboxUpdateSet: vi.fn(), + inboxUpdateWhere: vi.fn(), + markUnhealthy: vi.fn(async () => undefined), + recordAuthSaved: vi.fn(async () => undefined), + transition: vi.fn(async () => undefined), + findOwnerUserIdByWorkspaceId: vi.fn(async () => "owner-1"), +})) + +vi.mock("@chatbotx.io/database/client", () => ({ + db: { + execute: mocks.execute, + update: mocks.inboxUpdate, + }, + eq: vi.fn((column, value) => ({ column, value })), + sql: Object.assign( + (strings: TemplateStringsArray, ...values: unknown[]) => ({ + strings, + values, + }), + { identifier: (name: string) => name }, + ), +})) + +vi.mock("@chatbotx.io/database/repositories", () => ({ + connectionRepository: { + findByInboxId: mocks.findByInboxId, + findByIntegrationId: mocks.findByIntegrationId, + }, +})) + +vi.mock("@chatbotx.io/database/schema", () => ({ + inboxModel: { id: "id" }, +})) + +vi.mock("@chatbotx.io/redis", () => ({ + distributedLock: { runExclusive: mocks.runExclusive }, +})) + +vi.mock("../../connection/state-service", () => ({ + connectionStateService: { + markUnhealthy: mocks.markUnhealthy, + recordAuthSaved: mocks.recordAuthSaved, + transition: mocks.transition, + }, +})) + +vi.mock("../../workspace-member/service", () => ({ + workspaceMemberService: { + findOwnerUserIdByWorkspaceId: mocks.findOwnerUserIdByWorkspaceId, + }, +})) + +const { makeAuthStore } = await import("../auth-store") + +beforeEach(() => { + vi.clearAllMocks() + mocks.inboxUpdate.mockReturnValue({ set: mocks.inboxUpdateSet }) + mocks.inboxUpdateSet.mockReturnValue({ where: mocks.inboxUpdateWhere }) + mocks.execute.mockResolvedValue({ rows: [{ auth: { authType: "none" } }] }) + mocks.findOwnerUserIdByWorkspaceId.mockResolvedValue("owner-1") +}) + +describe("makeAuthStore.save", () => { + it("mirrors authExpiresAt onto the Connection row for an oauth2 auth", async () => { + mocks.findByInboxId.mockResolvedValue({ + id: "conn-1", + workspaceId: "ws-1", + }) + const store = makeAuthStore("messenger", { + id: "row-1", + inboxId: "inbox-1", + }) + await store.save({ + authType: "oauth2", + clientId: "c", + clientSecret: "s", + redirectUrl: "r", + tokens: { accessToken: "tok", expiresAt: "2030-01-01T00:00:00.000Z" }, + }) + expect(mocks.recordAuthSaved).toHaveBeenCalledWith({ + connectionId: "conn-1", + authExpiresAt: new Date("2030-01-01T00:00:00.000Z"), + }) + }) + + it("records a null authExpiresAt for a non-oauth2 auth", async () => { + mocks.findByInboxId.mockResolvedValue({ + id: "conn-1", + workspaceId: "ws-1", + }) + const store = makeAuthStore("smtp", { id: "row-1", inboxId: "inbox-1" }) + await store.save({ authType: "none" }) + expect(mocks.recordAuthSaved).toHaveBeenCalledWith({ + connectionId: "conn-1", + authExpiresAt: null, + }) + }) + + it("skips the Connection mirror when no matching row exists (pre-backfill)", async () => { + mocks.findByInboxId.mockResolvedValue(undefined) + const store = makeAuthStore("messenger", { + id: "row-1", + inboxId: "inbox-1", + }) + await store.save({ authType: "none" }) + expect(mocks.recordAuthSaved).not.toHaveBeenCalled() + }) +}) + +describe("makeAuthStore.markOffline", () => { + it("routes through connectionStateService.markUnhealthy with the resolved owner", async () => { + mocks.findByInboxId.mockResolvedValue({ + id: "conn-1", + workspaceId: "ws-1", + }) + const store = makeAuthStore("messenger", { + id: "row-1", + inboxId: "inbox-1", + }) + await store.markOffline?.() + expect(mocks.markUnhealthy).toHaveBeenCalledWith({ + connectionId: "conn-1", + ownerId: "owner-1", + }) + expect(mocks.inboxUpdate).not.toHaveBeenCalled() + }) + + it("falls back to a direct Inbox write when no Connection row exists yet", async () => { + mocks.findByInboxId.mockResolvedValue(undefined) + const store = makeAuthStore("messenger", { + id: "row-1", + inboxId: "inbox-1", + }) + await store.markOffline?.() + expect(mocks.markUnhealthy).not.toHaveBeenCalled() + expect(mocks.inboxUpdateSet).toHaveBeenCalledWith({ + status: "disconnected", + }) + }) + + it("no-ops for a workspace-level integration with no Connection and no inboxId", async () => { + const store = makeAuthStoreForTableFallback() + await store.markOffline?.() + expect(mocks.markUnhealthy).not.toHaveBeenCalled() + expect(mocks.inboxUpdate).not.toHaveBeenCalled() + }) +}) + +function makeAuthStoreForTableFallback() { + mocks.findByInboxId.mockResolvedValue(undefined) + mocks.findByIntegrationId.mockResolvedValue(undefined) + return makeAuthStore("claude", { id: "row-1" }) +} + +describe("makeAuthStore.recordHealth", () => { + it("transitions verify.ok on a healthy check", async () => { + mocks.findByInboxId.mockResolvedValue({ + id: "conn-1", + workspaceId: "ws-1", + }) + const store = makeAuthStore("messenger", { + id: "row-1", + inboxId: "inbox-1", + }) + await store.recordHealth?.({ ok: true }) + expect(mocks.transition).toHaveBeenCalledWith({ + connectionId: "conn-1", + event: "verify.ok", + }) + expect(mocks.markUnhealthy).not.toHaveBeenCalled() + }) + + it("marks unhealthy with token_revoked when the health check reports a revoked auth", async () => { + mocks.findByInboxId.mockResolvedValue({ + id: "conn-1", + workspaceId: "ws-1", + }) + const store = makeAuthStore("messenger", { + id: "row-1", + inboxId: "inbox-1", + }) + await store.recordHealth?.({ ok: false, revoked: true, error: "expired" }) + expect(mocks.markUnhealthy).toHaveBeenCalledWith({ + connectionId: "conn-1", + reason: "token_revoked", + ownerId: "owner-1", + }) + }) + + it("transitions verify.failed_non_auth for a non-auth health failure", async () => { + mocks.findByInboxId.mockResolvedValue({ + id: "conn-1", + workspaceId: "ws-1", + }) + const store = makeAuthStore("messenger", { + id: "row-1", + inboxId: "inbox-1", + }) + await store.recordHealth?.({ + ok: false, + revoked: false, + error: "rate_limited", + }) + expect(mocks.transition).toHaveBeenCalledWith({ + connectionId: "conn-1", + event: "verify.failed_non_auth", + reason: "verify_failed", + ownerId: "owner-1", + }) + }) + + it("no-ops when no Connection row exists yet", async () => { + mocks.findByInboxId.mockResolvedValue(undefined) + const store = makeAuthStore("messenger", { + id: "row-1", + inboxId: "inbox-1", + }) + await store.recordHealth?.({ ok: true }) + expect(mocks.transition).not.toHaveBeenCalled() + }) +}) diff --git a/packages/business/src/integration-context/auth-store.ts b/packages/business/src/integration-context/auth-store.ts index 8765de184e..b705157ed3 100644 --- a/packages/business/src/integration-context/auth-store.ts +++ b/packages/business/src/integration-context/auth-store.ts @@ -1,7 +1,15 @@ import { db, eq, sql } from "@chatbotx.io/database/client" +import { connectionRepository } from "@chatbotx.io/database/repositories" import { inboxModel } from "@chatbotx.io/database/schema" import { distributedLock } from "@chatbotx.io/redis" -import { type AuthStore, type AuthValue, SdkException } from "@chatbotx.io/sdk" +import { + type AuthStore, + type AuthValue, + type ConnectionHealth, + SdkException, +} from "@chatbotx.io/sdk" +import { connectionStateService } from "../connection/state-service" +import { workspaceMemberService } from "../workspace-member/service" const REFRESH_LOCK_TIMEOUT_SECONDS = 10 @@ -44,6 +52,26 @@ export const makeAuthStoreForTable = ( integration: AuthStoreIntegrationRow, ): AuthStore => { const lockKey = `auth:refresh:${lockKeyPrefix}:${integration.id}` + /** + * Resolves the `Connection` row mirroring this `Integration` (or + * workspace-integration satellite) row, so `markOffline`/`recordHealth` + * can route through `connectionStateService` instead of writing `Inbox` + * directly. Returns `undefined` for a row predating the Phase 1 backfill + * — callers fall back to the legacy direct write in that case. + */ + const resolveConnection = async () => { + if (integration.inboxId) { + return await connectionRepository.findByInboxId({ + inboxId: integration.inboxId, + }) + } + if (integration.integrationId) { + return await connectionRepository.findByIntegrationId({ + integrationId: integration.integrationId, + }) + } + return + } return { load: async () => { @@ -61,6 +89,19 @@ export const makeAuthStoreForTable = ( await db.execute( sql`UPDATE ${sql.identifier(tableName)} SET auth = ${JSON.stringify(auth)}::jsonb WHERE "id" = ${integration.id}`, ) + const connection = await resolveConnection() + if (!connection) { + // Pre-backfill fallback: no `Connection` row to mirror onto yet. + return + } + const authExpiresAt = + auth.authType === "oauth2" && auth.tokens.expiresAt + ? new Date(auth.tokens.expiresAt) + : null + await connectionStateService.recordAuthSaved({ + connectionId: connection.id, + authExpiresAt, + }) }, withLock: (fn) => distributedLock.runExclusive({ @@ -69,6 +110,20 @@ export const makeAuthStoreForTable = ( fn, }), markOffline: async () => { + const connection = await resolveConnection() + if (connection) { + const ownerId = + await workspaceMemberService.findOwnerUserIdByWorkspaceId({ + workspaceId: connection.workspaceId, + }) + await connectionStateService.markUnhealthy({ + connectionId: connection.id, + ownerId, + }) + return + } + // Pre-backfill fallback: no `Connection` row exists yet for this + // integration — fall back to the legacy direct `Inbox` write. if (!integration.inboxId) { // Workspace-level integration (no inbox) — nothing to disconnect. return @@ -78,6 +133,37 @@ export const makeAuthStoreForTable = ( .set({ status: "disconnected" }) .where(eq(inboxModel.id, integration.inboxId)) }, + recordHealth: async (health: ConnectionHealth) => { + const connection = await resolveConnection() + if (!connection) { + // Pre-backfill fallback: nothing to mirror the health check onto yet. + return + } + if (health.ok) { + await connectionStateService.transition({ + connectionId: connection.id, + event: "verify.ok", + }) + return + } + const ownerId = await workspaceMemberService.findOwnerUserIdByWorkspaceId( + { workspaceId: connection.workspaceId }, + ) + if (health.revoked) { + await connectionStateService.markUnhealthy({ + connectionId: connection.id, + reason: "token_revoked", + ownerId, + }) + return + } + await connectionStateService.transition({ + connectionId: connection.id, + event: "verify.failed_non_auth", + reason: "verify_failed", + ownerId, + }) + }, } } diff --git a/packages/business/src/integration/__tests__/find-token-refresh-errors.test.ts b/packages/business/src/integration/__tests__/find-token-refresh-errors.test.ts new file mode 100644 index 0000000000..348c3f35eb --- /dev/null +++ b/packages/business/src/integration/__tests__/find-token-refresh-errors.test.ts @@ -0,0 +1,93 @@ +import { beforeEach, describe, expect, it, vi } from "vitest" + +const mocks = vi.hoisted(() => ({ + list: vi.fn(), +})) + +vi.mock("../../connection/state-service", () => ({ + connectionStateService: { list: mocks.list }, +})) + +vi.mock("@chatbotx.io/database/client", () => ({ + and: vi.fn(), + db: {}, + eq: vi.fn(), + exists: vi.fn(), + isNull: vi.fn(), + ne: vi.fn(), + or: vi.fn(), +})) + +vi.mock("@chatbotx.io/database/schema", () => ({ + integrationMetaCatalogModel: {}, + integrationModel: {}, +})) + +const { integrationService } = await import("../service") + +const connection = (overrides: Partial> = {}) => ({ + id: "conn-1", + provider: "messenger", + displayName: "My Page", + lastError: "refresh failed", + ...overrides, +}) + +beforeEach(() => { + vi.clearAllMocks() +}) + +describe("integrationService.findTokenRefreshErrorsByWorkspaceId", () => { + it("queries connectionStateService.list scoped to channel connections in needs_reauth/degraded", async () => { + mocks.list.mockResolvedValue({ data: [] }) + await integrationService.findTokenRefreshErrorsByWorkspaceId("ws-1") + expect(mocks.list).toHaveBeenCalledWith({ + workspaceId: "ws-1", + kind: "channel", + status: ["needs_reauth", "degraded"], + }) + }) + + it("maps a matching auto-refresh channel connection onto the legacy DTO", async () => { + mocks.list.mockResolvedValue({ data: [connection()] }) + const result = + await integrationService.findTokenRefreshErrorsByWorkspaceId("ws-1") + expect(result).toEqual([ + { + id: "conn-1", + channel: "messenger", + name: "My Page", + error: "refresh failed", + }, + ]) + }) + + it("preserves the instagram vs instagramFacebook provider distinction", async () => { + mocks.list.mockResolvedValue({ + data: [connection({ id: "conn-2", provider: "instagramFacebook" })], + }) + const result = + await integrationService.findTokenRefreshErrorsByWorkspaceId("ws-1") + expect(result).toEqual([ + expect.objectContaining({ channel: "instagramFacebook" }), + ]) + }) + + it("excludes a connection whose provider does not auto-refresh (e.g. smtp)", async () => { + mocks.list.mockResolvedValue({ + data: [connection({ provider: "smtp" })], + }) + const result = + await integrationService.findTokenRefreshErrorsByWorkspaceId("ws-1") + expect(result).toEqual([]) + }) + + it("excludes a connection with no lastError", async () => { + mocks.list.mockResolvedValue({ + data: [connection({ lastError: null })], + }) + const result = + await integrationService.findTokenRefreshErrorsByWorkspaceId("ws-1") + expect(result).toEqual([]) + }) +}) diff --git a/packages/business/src/integration/service.ts b/packages/business/src/integration/service.ts index 92a72d2f97..67e7dccef3 100644 --- a/packages/business/src/integration/service.ts +++ b/packages/business/src/integration/service.ts @@ -3,22 +3,17 @@ import { db, eq, exists, - isNotNull, isNull, ne, or, } from "@chatbotx.io/database/client" import { - integrationInstagramModel, - integrationMessengerModel, integrationMetaCatalogModel, integrationModel, - integrationTiktokModel, - integrationWhatsappModel, - integrationZaloModel, } from "@chatbotx.io/database/schema" import type { IntegrationModel } from "@chatbotx.io/database/types" import { BaseService } from "../base.service" +import { connectionStateService } from "../connection/state-service" export type TokenRefreshErrorChannel = | "zalo" @@ -74,119 +69,47 @@ class IntegrationService extends BaseService { } /** - * Channel integrations whose daily token-refresh cron last failed - * (`tokenRefreshError` set), across every channel that supports automatic - * refresh. Used to warn the workspace owner that a channel needs a manual - * reconnect before it silently stops sending/receiving messages. + * Channel integrations whose token-refresh last failed — delegates to + * `connectionStateService.list` (the `Connection` domain now owns + * `needs_reauth`/`degraded` status), filtered to the channels that + * actually auto-refresh, and mapped back onto this method's original DTO + * so the layout banner (`TokenRefreshErrorDialog`) is unchanged. */ async findTokenRefreshErrorsByWorkspaceId( workspaceId: string, ): Promise { - const [zalos, tiktoks, instagrams, messengers, whatsapps] = - await Promise.all([ - db - .select({ - id: integrationZaloModel.id, - name: integrationZaloModel.name, - error: integrationZaloModel.tokenRefreshError, - }) - .from(integrationZaloModel) - .where( - and( - eq(integrationZaloModel.workspaceId, workspaceId), - isNotNull(integrationZaloModel.tokenRefreshError), - ), - ), - db - .select({ - id: integrationTiktokModel.id, - name: integrationTiktokModel.name, - error: integrationTiktokModel.tokenRefreshError, - }) - .from(integrationTiktokModel) - .where( - and( - eq(integrationTiktokModel.workspaceId, workspaceId), - isNotNull(integrationTiktokModel.tokenRefreshError), - ), - ), - db - .select({ - id: integrationInstagramModel.id, - name: integrationInstagramModel.name, - error: integrationInstagramModel.tokenRefreshError, - type: integrationInstagramModel.type, - }) - .from(integrationInstagramModel) - .where( - and( - eq(integrationInstagramModel.workspaceId, workspaceId), - isNotNull(integrationInstagramModel.tokenRefreshError), - ), - ), - db - .select({ - id: integrationMessengerModel.id, - name: integrationMessengerModel.name, - error: integrationMessengerModel.tokenRefreshError, - }) - .from(integrationMessengerModel) - .where( - and( - eq(integrationMessengerModel.workspaceId, workspaceId), - isNotNull(integrationMessengerModel.tokenRefreshError), - ), - ), - db - .select({ - id: integrationWhatsappModel.id, - name: integrationWhatsappModel.name, - error: integrationWhatsappModel.tokenRefreshError, - }) - .from(integrationWhatsappModel) - .where( - and( - eq(integrationWhatsappModel.workspaceId, workspaceId), - isNotNull(integrationWhatsappModel.tokenRefreshError), - ), - ), - ]) - - return [ - ...zalos.map((row) => ({ - id: row.id, - channel: "zalo" as const, - name: row.name, - error: row.error as string, - })), - ...tiktoks.map((row) => ({ - id: row.id, - channel: "tiktok" as const, - name: row.name, - error: row.error as string, - })), - ...instagrams.map((row) => ({ - id: row.id, - channel: - row.type === "facebook" - ? ("instagramFacebook" as const) - : ("instagram" as const), - name: row.name, - error: row.error as string, - })), - ...messengers.map((row) => ({ - id: row.id, - channel: "messenger" as const, - name: row.name, - error: row.error as string, - })), - ...whatsapps.map((row) => ({ - id: row.id, - channel: "whatsapp" as const, - name: row.name, - error: row.error as string, - })), - ] + const autoRefreshChannels = new Set([ + "zalo", + "tiktok", + "instagram", + "instagramFacebook", + "messenger", + "whatsapp", + ]) + const { data } = await connectionStateService.list({ + workspaceId, + kind: "channel", + status: ["needs_reauth", "degraded"], + }) + return data.flatMap((connection) => { + if ( + !( + autoRefreshChannels.has( + connection.provider as TokenRefreshErrorChannel, + ) && connection.lastError + ) + ) { + return [] + } + return [ + { + id: connection.id, + channel: connection.provider as TokenRefreshErrorChannel, + name: connection.displayName, + error: connection.lastError, + }, + ] + }) } /** diff --git a/packages/connections/__tests__/registry.test.ts b/packages/connections/__tests__/registry.test.ts new file mode 100644 index 0000000000..51c636db44 --- /dev/null +++ b/packages/connections/__tests__/registry.test.ts @@ -0,0 +1,79 @@ +import { integrationTypes } from "@chatbotx.io/database/partials" +import { CHANNEL_CAPABILITIES, channelTypes } from "@chatbotx.io/utils/channel" +import { describe, expect, it } from "vitest" +import { CONNECTION_REGISTRY } from "../src/registry" + +const channelToIntegrationTable = (channel: string): string => { + const integrationName = channel + .split("_") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join("") + return `Integration${integrationName}` +} + +describe("CONNECTION_REGISTRY", () => { + it("has an entry (possibly null) for every IntegrationType", () => { + for (const type of integrationTypes.options) { + expect(Object.hasOwn(CONNECTION_REGISTRY, type)).toBe(true) + } + }) + + it("every non-null adapter's provider is populated, and store is populated except chatbotx's documented no-satellite-table exception", () => { + for (const [type, adapter] of Object.entries(CONNECTION_REGISTRY)) { + if (!adapter) { + continue + } + expect(adapter.provider, `${type}.provider`).toBeDefined() + if (type === "chatbotx") { + // Internal built-in channel: no `Integration` satellite + // table exists — the `Inbox` row itself is the whole connection. + expect(adapter.store, `${type}.store`).toBeUndefined() + continue + } + expect(adapter.store, `${type}.store`).toBeDefined() + } + }) + + it("kind=channel adapters map onto a real ChannelType (directly, via instagramFacebook -> instagram, or chatbotx's documented exception)", () => { + const channelSet = new Set(channelTypes.options) + for (const [type, adapter] of Object.entries(CONNECTION_REGISTRY)) { + if (adapter?.provider.kind !== "channel" || type === "chatbotx") { + // `chatbotx` is a channel-shaped internal integration (it has + // `channels` handlers) with no formal `ChannelType` membership and + // no `Inbox.channel = "chatbotx"` value — it is never offered + // through the normal channel-connect surface. + continue + } + const impliedChannel = type === "instagramFacebook" ? "instagram" : type + expect( + channelSet.has(impliedChannel), + `${type} -> ${impliedChannel}`, + ).toBe(true) + } + }) + + it("channel-kind adapters keyed directly by ChannelType use the matching Integration table", () => { + for (const [type, adapter] of Object.entries(CONNECTION_REGISTRY)) { + if (adapter?.provider.kind !== "channel") { + continue + } + if (!(channelTypes.options as readonly string[]).includes(type)) { + continue + } + expect(adapter.store.table).toBe(channelToIntegrationTable(type)) + } + }) + + it("credentialType is set iff the underlying channel requires a platform credential", () => { + for (const channel of channelTypes.options) { + const adapter = + CONNECTION_REGISTRY[channel as keyof typeof CONNECTION_REGISTRY] + if (!adapter) { + continue + } + const requiresCredential = + CHANNEL_CAPABILITIES[channel].requiresCredential + expect(Boolean(adapter.credentialType), channel).toBe(requiresCredential) + } + }) +}) diff --git a/packages/connections/__tests__/service.test.ts b/packages/connections/__tests__/service.test.ts new file mode 100644 index 0000000000..3b1dbae805 --- /dev/null +++ b/packages/connections/__tests__/service.test.ts @@ -0,0 +1,1417 @@ +import { channelLimitReachedException } from "@chatbotx.io/business/errors" +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" + +const mocks = vi.hoisted(() => ({ + findByIdForWorkspace: vi.fn(), + findById: vi.fn(), + findByProviderSourceId: vi.fn(), + findByProviderAndSourceIdAnyWorkspace: vi.fn(async () => undefined), + insert: vi.fn(), + update: vi.fn(), + findOwnerUserIdByWorkspaceId: vi.fn(async () => "owner-1"), + transition: vi.fn(async (input: Record) => ({ + id: input.connectionId, + status: "connected", + })), + markUnhealthy: vi.fn(async (input: Record) => ({ + id: input.connectionId, + status: "needs_reauth", + })), + recordAuthSaved: vi.fn(async (input: Record) => ({ + id: input.connectionId, + status: "connected", + })), + loadAuthByForeignKey: vi.fn(async () => ({ authType: "none" })), + saveAuthByForeignKey: vi.fn(async () => undefined), + deleteRowByForeignKey: vi.fn(async () => undefined), + insertRow: vi.fn(async () => ({ id: "sat-1", integrationId: "int-1" })), + disconnect: vi.fn(async () => undefined), + unsubscribe: vi.fn(async () => undefined), + subscribe: vi.fn(async () => undefined), + verify: vi.fn(async () => ({ ok: true })), + ensureFreshAuth: vi.fn(async () => undefined), + fromCredentials: vi.fn(async () => ({ + authType: "secretText", + secretText: "sk-live", + })), + isUniqueViolationError: vi.fn(() => false), + transaction: vi.fn(async (fn: (tx: unknown) => unknown) => await fn("tx")), + createSession: vi.fn(), + findByNonce: vi.fn(), + submitInput: vi.fn(), + attachAuthorization: vi.fn(), + recordResults: vi.fn(), + failSession: vi.fn(), + authorizeUrl: vi.fn(() => "https://provider.example.com/authorize"), + exchangeCode: vi.fn(async () => ({ + authType: "oauth2", + clientId: "id", + clientSecret: "secret", + redirectUrl: "https://x", + tokens: { accessToken: "tok" }, + })), + listCandidates: vi.fn(async () => [ + { sourceId: "page-1", displayName: "Page One" }, + ]), + encryptObject: vi.fn(async () => ({ + iv: "iv", + ciphertext: "c", + keyId: "k", + })), + decryptObject: vi.fn(), + findSessionByIdForWorkspace: vi.fn(), + claimTarget: vi.fn(async () => true), + inboxCreate: vi.fn(async () => ({ + inbox: { id: "inbox-new" }, + wasCreated: true, + })), +})) + +vi.mock("@chatbotx.io/database/repositories", () => ({ + connectionRepository: { + findByIdForWorkspace: mocks.findByIdForWorkspace, + findById: mocks.findById, + findByProviderSourceId: mocks.findByProviderSourceId, + findByProviderAndSourceIdAnyWorkspace: + mocks.findByProviderAndSourceIdAnyWorkspace, + insert: mocks.insert, + update: mocks.update, + }, +})) + +vi.mock("@chatbotx.io/database/client", () => ({ + db: { transaction: mocks.transaction }, + isUniqueViolationError: mocks.isUniqueViolationError, +})) + +vi.mock("@chatbotx.io/encryption", () => ({ + encryptUtils: { + encryptObject: mocks.encryptObject, + decryptObject: mocks.decryptObject, + }, +})) + +vi.mock("@chatbotx.io/business", () => ({ + workspaceMemberService: { + findOwnerUserIdByWorkspaceId: mocks.findOwnerUserIdByWorkspaceId, + }, + inboxService: { create: mocks.inboxCreate }, +})) + +vi.mock("@chatbotx.io/business/connection", () => ({ + connectionStateService: { + transition: mocks.transition, + markUnhealthy: mocks.markUnhealthy, + recordAuthSaved: mocks.recordAuthSaved, + }, + isActiveConnectionStatus: (status: string) => + status === "connected" || status === "degraded", +})) + +vi.mock("@chatbotx.io/business/connect-session", () => ({ + connectSessionService: { + create: mocks.createSession, + findByNonce: mocks.findByNonce, + findByIdForWorkspace: mocks.findSessionByIdForWorkspace, + claimTarget: mocks.claimTarget, + submitInput: mocks.submitInput, + attachAuthorization: mocks.attachAuthorization, + recordResults: mocks.recordResults, + fail: mocks.failSession, + }, +})) + +vi.mock("@chatbotx.io/business/errors", () => { + class TestChatbotXException extends Error { + code: string + httpStatusCode: number + constructor(message: string, code: string, httpStatusCode = 400) { + super(message) + this.code = code + this.httpStatusCode = httpStatusCode + } + } + return { + ChatbotXException: TestChatbotXException, + channelLimitReachedException: () => + new TestChatbotXException( + "Channel limit reached for this plan", + "channelLimitReached", + 409, + ), + connectionInactiveException: () => + new TestChatbotXException( + "This connection is not active.", + "connectionInactive", + 409, + ), + connectionNotConfiguredException: (provider: string) => + new TestChatbotXException( + `Connection provider "${provider}" is not configured.`, + "connectionNotConfigured", + 500, + ), + connectionNotRefreshableException: (provider: string) => + new TestChatbotXException( + `Connection provider "${provider}" does not support refresh.`, + "connectionNotRefreshable", + 400, + ), + connectionAlreadyConnectedException: () => + new TestChatbotXException( + "This provider is already connected in this workspace.", + "connectionAlreadyConnected", + 409, + ), + connectionWrongStrategyException: (provider: string) => + new TestChatbotXException( + `Connection provider "${provider}" does not accept direct credentials.`, + "connectionWrongStrategy", + 400, + ), + connectionCredentialsRejectedException: (message: string) => + new TestChatbotXException(message, "connectionCredentialsRejected", 400), + connectionNotOAuthException: (provider: string) => + new TestChatbotXException( + `Connection provider "${provider}" does not support an OAuth connect flow.`, + "connectionNotOAuth", + 400, + ), + connectionStateMismatchException: () => + new TestChatbotXException( + "This connect session could not be verified.", + "connectionStateMismatch", + 400, + ), + connectionNoCandidatesException: () => + new TestChatbotXException( + "No connectable accounts were found for this authorization.", + "connectionNoCandidates", + 400, + ), + connectionIdentityMismatchException: () => + new TestChatbotXException( + "The reauthorized account does not match the connection being reconnected.", + "connectionIdentityMismatch", + 400, + ), + connectSessionExpiredException: (message: string) => + new TestChatbotXException(message, "connectSessionExpired", 400), + validationException: (field: string, message: string) => { + const error = new TestChatbotXException(message, "validation", 422) + return Object.assign(error, { field }) + }, + toPublicErrorMessage: (error: unknown, fallback: string) => + error instanceof Error ? error.message : fallback, + notFoundException: (message: string) => + new TestChatbotXException(message, "notFound", 404), + } +}) + +let refreshAuthHandler: unknown = vi.fn() + +const mockAdapter = { + provider: { + kind: "channel", + strategy: "api_key", + configFields: [ + { name: "apiKey", type: "secret", required: true, labelKey: "x" }, + ], + webhook: { unsubscribe: mocks.unsubscribe, subscribe: mocks.subscribe }, + verify: mocks.verify, + fromCredentials: mocks.fromCredentials, + describe: () => ({ sourceId: "workspace", displayName: "Test Provider" }), + authorizeUrl: mocks.authorizeUrl, + exchangeCode: mocks.exchangeCode, + listCandidates: mocks.listCandidates, + multiAccount: true, + }, + integration: { + disconnect: mocks.disconnect, + ensureFreshAuth: mocks.ensureFreshAuth, + get refreshAuth() { + return refreshAuthHandler + }, + }, + store: { + loadAuthByForeignKey: mocks.loadAuthByForeignKey, + saveAuthByForeignKey: mocks.saveAuthByForeignKey, + deleteRowByForeignKey: mocks.deleteRowByForeignKey, + insertRow: mocks.insertRow, + onDisconnect: "delete_row", + duplicateConstraint: "Test_workspaceId_key", + }, +} + +vi.mock("../src/registry", () => ({ + CONNECTION_REGISTRY: new Proxy( + {}, + { + get: (_target, provider) => + provider === "unregistered" ? null : mockAdapter, + }, + ), +})) + +vi.mock("../src/logger", () => ({ + logger: { warn: vi.fn(), error: vi.fn(), info: vi.fn() }, +})) + +const { connectionService } = await import("../src/service") + +const baseConnection = (overrides: Partial> = {}) => ({ + id: "conn-1", + workspaceId: "ws-1", + provider: "messenger", + kind: "channel", + status: "connected", + inboxId: "inbox-1", + integrationId: null, + ...overrides, +}) + +beforeEach(() => { + vi.clearAllMocks() + refreshAuthHandler = vi.fn() + mockAdapter.provider.kind = "channel" + mocks.findOwnerUserIdByWorkspaceId.mockResolvedValue("owner-1") + mocks.loadAuthByForeignKey.mockResolvedValue({ authType: "none" }) + mocks.verify.mockResolvedValue({ ok: true }) + mocks.fromCredentials.mockResolvedValue({ + authType: "secretText", + secretText: "sk-live", + }) + mocks.isUniqueViolationError.mockReturnValue(false) + mocks.findByProviderSourceId.mockResolvedValue(undefined) + mocks.findByProviderAndSourceIdAnyWorkspace.mockResolvedValue(undefined) + mocks.insertRow.mockResolvedValue({ id: "sat-1", integrationId: "int-1" }) + mocks.insert.mockImplementation(async (values: Record) => ({ + id: "conn-new", + ...values, + })) + mocks.transition.mockImplementation(async (input) => ({ + id: input.connectionId, + status: "disconnected", + })) + mocks.markUnhealthy.mockImplementation(async (input) => ({ + id: input.connectionId, + status: "needs_reauth", + })) + mocks.authorizeUrl.mockReturnValue("https://provider.example.com/authorize") + mocks.exchangeCode.mockResolvedValue({ + authType: "oauth2", + clientId: "id", + clientSecret: "secret", + redirectUrl: "https://x", + tokens: { accessToken: "tok" }, + }) + mocks.listCandidates.mockResolvedValue([ + { sourceId: "page-1", displayName: "Page One" }, + ]) + mocks.encryptObject.mockResolvedValue({ + iv: "iv", + ciphertext: "c", + keyId: "k", + }) + mocks.createSession.mockResolvedValue({ + session: { id: "session-1", workspaceId: "ws-1", provider: "messenger" }, + nonce: "nonce-abc", + }) + mocks.findByNonce.mockResolvedValue({ + id: "session-1", + workspaceId: "ws-1", + provider: "messenger", + status: "pending", + }) + mocks.submitInput.mockImplementation( + async (input: { id: string; nextAction: unknown }) => ({ + id: input.id, + nextAction: input.nextAction, + }), + ) + mocks.attachAuthorization.mockImplementation( + async (input: Record) => ({ + id: input.id, + status: "awaiting_selection", + targets: input.targets, + }), + ) + mocks.failSession.mockImplementation( + async (input: Record) => ({ + id: input.id, + status: "failed", + }), + ) + mocks.findById.mockResolvedValue(baseConnection()) + mocks.recordResults.mockImplementation( + async (input: Record) => ({ + id: input.id, + status: "completed", + }), + ) +}) + +describe("ConnectionService.disconnect", () => { + it("throws notFound when the connection does not belong to the workspace", async () => { + mocks.findByIdForWorkspace.mockResolvedValue(undefined) + await expect( + connectionService.disconnect({ + connectionId: "conn-1", + workspaceId: "ws-1", + }), + ).rejects.toThrow("Connection not found") + }) + + it("calls provider disconnect + webhook unsubscribe + deletes the satellite row, then transitions", async () => { + mocks.findByIdForWorkspace.mockResolvedValue(baseConnection()) + const result = await connectionService.disconnect({ + connectionId: "conn-1", + workspaceId: "ws-1", + }) + expect(mocks.loadAuthByForeignKey).toHaveBeenCalledWith("inbox-1") + expect(mocks.disconnect).toHaveBeenCalledWith({ authType: "none" }) + expect(mocks.unsubscribe).toHaveBeenCalledWith({ + auth: { authType: "none" }, + }) + expect(mocks.deleteRowByForeignKey).toHaveBeenCalledWith("inbox-1") + expect(mocks.transition).toHaveBeenCalledWith({ + connectionId: "conn-1", + event: "user.disconnect", + ownerId: "owner-1", + }) + expect(result.status).toBe("disconnected") + }) + + it("never resolves an ownerId (channels quota) for a kind:integration connection", async () => { + mocks.findByIdForWorkspace.mockResolvedValue( + baseConnection({ + kind: "integration", + inboxId: null, + integrationId: "int-1", + }), + ) + await connectionService.disconnect({ + connectionId: "conn-1", + workspaceId: "ws-1", + }) + expect(mocks.findOwnerUserIdByWorkspaceId).not.toHaveBeenCalled() + expect(mocks.transition).toHaveBeenCalledWith({ + connectionId: "conn-1", + event: "user.disconnect", + ownerId: undefined, + }) + }) + + it("proceeds to the state transition even when provider-side teardown throws", async () => { + mocks.findByIdForWorkspace.mockResolvedValue(baseConnection()) + mocks.disconnect.mockRejectedValue(new Error("upstream 500")) + await connectionService.disconnect({ + connectionId: "conn-1", + workspaceId: "ws-1", + }) + expect(mocks.deleteRowByForeignKey).toHaveBeenCalledWith("inbox-1") + expect(mocks.transition).toHaveBeenCalledWith( + expect.objectContaining({ event: "user.disconnect" }), + ) + }) +}) + +describe("ConnectionService.refresh", () => { + it("throws connectionInactive for a disconnected connection", async () => { + mocks.findByIdForWorkspace.mockResolvedValue( + baseConnection({ status: "disconnected" }), + ) + await expect( + connectionService.refresh({ + connectionId: "conn-1", + workspaceId: "ws-1", + }), + ).rejects.toMatchObject({ code: "connectionInactive" }) + }) + + it("throws connectionNotRefreshable when the provider has no refreshAuth", async () => { + mocks.findByIdForWorkspace.mockResolvedValue(baseConnection()) + refreshAuthHandler = undefined + await expect( + connectionService.refresh({ + connectionId: "conn-1", + workspaceId: "ws-1", + }), + ).rejects.toMatchObject({ code: "connectionNotRefreshable" }) + }) + + it("calls ensureFreshAuth with force:true and returns the reloaded connection", async () => { + mocks.findByIdForWorkspace.mockResolvedValue(baseConnection()) + mocks.findById.mockResolvedValue(baseConnection({ status: "connected" })) + const result = await connectionService.refresh({ + connectionId: "conn-1", + workspaceId: "ws-1", + }) + expect(mocks.ensureFreshAuth).toHaveBeenCalledWith( + expect.objectContaining({ auth: { authType: "none" } }), + { force: true }, + ) + expect(result.id).toBe("conn-1") + }) + + it("authStore.save persists via saveAuthByForeignKey and records authExpiresAt", async () => { + mocks.findByIdForWorkspace.mockResolvedValue(baseConnection()) + mocks.findById.mockResolvedValue(baseConnection()) + mocks.ensureFreshAuth.mockImplementation(async (ctx) => { + await ctx.authStore.save({ + authType: "oauth2", + tokens: { accessToken: "new", expiresAt: "2030-01-01T00:00:00.000Z" }, + }) + }) + await connectionService.refresh({ + connectionId: "conn-1", + workspaceId: "ws-1", + }) + expect(mocks.saveAuthByForeignKey).toHaveBeenCalledWith( + "inbox-1", + expect.objectContaining({ authType: "oauth2" }), + ) + expect(mocks.recordAuthSaved).toHaveBeenCalledWith({ + connectionId: "conn-1", + authExpiresAt: new Date("2030-01-01T00:00:00.000Z"), + }) + }) +}) + +describe("ConnectionService.verify", () => { + it("transitions to verify.ok on a healthy check", async () => { + mocks.findByIdForWorkspace.mockResolvedValue(baseConnection()) + mocks.verify.mockResolvedValue({ ok: true }) + await connectionService.verify({ + connectionId: "conn-1", + workspaceId: "ws-1", + }) + expect(mocks.transition).toHaveBeenCalledWith({ + connectionId: "conn-1", + event: "verify.ok", + ownerId: "owner-1", + }) + }) + + it("marks unhealthy with token_revoked when the health check reports a revoked auth", async () => { + mocks.findByIdForWorkspace.mockResolvedValue(baseConnection()) + mocks.verify.mockResolvedValue({ + ok: false, + revoked: true, + error: "expired", + }) + await connectionService.verify({ + connectionId: "conn-1", + workspaceId: "ws-1", + }) + expect(mocks.markUnhealthy).toHaveBeenCalledWith({ + connectionId: "conn-1", + reason: "token_revoked", + ownerId: "owner-1", + }) + }) + + it("transitions to verify.failed_non_auth for a non-auth health failure", async () => { + mocks.findByIdForWorkspace.mockResolvedValue(baseConnection()) + mocks.verify.mockResolvedValue({ + ok: false, + revoked: false, + error: "rate_limited", + }) + await connectionService.verify({ + connectionId: "conn-1", + workspaceId: "ws-1", + }) + expect(mocks.transition).toHaveBeenCalledWith({ + connectionId: "conn-1", + event: "verify.failed_non_auth", + reason: "verify_failed", + ownerId: "owner-1", + }) + }) +}) + +describe("ConnectionService.connectFromCredentials", () => { + it("throws connectionWrongStrategy when the provider strategy is not credential-based", async () => { + mockAdapter.provider.strategy = "oauth_redirect" as never + await expect( + connectionService.connectFromCredentials({ + workspaceId: "ws-1", + provider: "claude", + config: { apiKey: "sk-live" }, + }), + ).rejects.toMatchObject({ code: "connectionWrongStrategy" }) + mockAdapter.provider.strategy = "api_key" + }) + + it("throws a field-scoped validation error when a required config field is missing", async () => { + await expect( + connectionService.connectFromCredentials({ + workspaceId: "ws-1", + provider: "claude", + config: {}, + }), + ).rejects.toMatchObject({ code: "validation", field: "apiKey" }) + expect(mocks.fromCredentials).not.toHaveBeenCalled() + }) + + it("throws connectionCredentialsRejected when fromCredentials rejects the config", async () => { + mocks.fromCredentials.mockRejectedValue(new Error("Invalid API key")) + await expect( + connectionService.connectFromCredentials({ + workspaceId: "ws-1", + provider: "claude", + config: { apiKey: "sk-bad" }, + }), + ).rejects.toMatchObject({ code: "connectionCredentialsRejected" }) + }) + + it("throws connectionAlreadyConnected when an active connection already exists for this provider", async () => { + mocks.findByProviderSourceId.mockResolvedValue({ + id: "conn-1", + status: "connected", + }) + await expect( + connectionService.connectFromCredentials({ + workspaceId: "ws-1", + provider: "claude", + config: { apiKey: "sk-live" }, + }), + ).rejects.toMatchObject({ code: "connectionAlreadyConnected" }) + expect(mocks.insertRow).not.toHaveBeenCalled() + }) + + it("allowUpdate: true replaces an already-connected provider's config in place instead of throwing", async () => { + mocks.findByProviderSourceId.mockResolvedValue({ + id: "conn-existing", + status: "connected", + }) + + const result = await connectionService.connectFromCredentials({ + workspaceId: "ws-1", + provider: "claude", + config: { apiKey: "sk-new" }, + allowUpdate: true, + }) + + expect(mocks.insertRow).toHaveBeenCalledOnce() + expect(mocks.update).toHaveBeenCalledWith( + expect.objectContaining({ id: "conn-existing" }), + "tx", + ) + expect(mocks.transition).toHaveBeenCalledWith({ + connectionId: "conn-existing", + event: "connect.completed", + ownerId: "owner-1", + tx: "tx", + }) + expect(result.id).toBe("conn-existing") + }) + + it("maps a duplicate-constraint insert failure to connectionAlreadyConnected", async () => { + mocks.insertRow.mockRejectedValue(new Error("unique violation")) + mocks.isUniqueViolationError.mockReturnValue(true) + await expect( + connectionService.connectFromCredentials({ + workspaceId: "ws-1", + provider: "claude", + config: { apiKey: "sk-live" }, + }), + ).rejects.toMatchObject({ code: "connectionAlreadyConnected" }) + }) + + it("inserts a fresh Connection as disconnected then transitions it to connected inside one transaction", async () => { + const result = await connectionService.connectFromCredentials({ + workspaceId: "ws-1", + provider: "claude", + config: { apiKey: "sk-live" }, + actorUserId: "user-1", + }) + + expect(mocks.transaction).toHaveBeenCalledOnce() + expect(mocks.insertRow).toHaveBeenCalledWith( + expect.objectContaining({ workspaceId: "ws-1" }), + "tx", + ) + expect(mocks.insert).toHaveBeenCalledWith( + expect.objectContaining({ + workspaceId: "ws-1", + provider: "claude", + sourceId: "workspace", + integrationId: "int-1", + status: "disconnected", + createdBy: "user-1", + }), + "tx", + ) + expect(mocks.transition).toHaveBeenCalledWith({ + connectionId: "conn-new", + event: "connect.completed", + ownerId: "owner-1", + tx: "tx", + }) + expect(result.id).toBe("conn-new") + }) + + it("subscribes the provider webhook after a successful connect", async () => { + await connectionService.connectFromCredentials({ + workspaceId: "ws-1", + provider: "claude", + config: { apiKey: "sk-live" }, + }) + + expect(mocks.subscribe).toHaveBeenCalledWith({ + auth: { authType: "secretText", secretText: "sk-live" }, + }) + }) + + it("degrades the connection when the post-connect webhook subscribe fails, without failing the connect itself", async () => { + mocks.subscribe.mockRejectedValueOnce(new Error("webhook endpoint down")) + + const result = await connectionService.connectFromCredentials({ + workspaceId: "ws-1", + provider: "claude", + config: { apiKey: "sk-live" }, + }) + + expect(mocks.transition).toHaveBeenCalledWith({ + connectionId: "conn-new", + event: "connect.completed", + ownerId: "owner-1", + tx: "tx", + }) + expect(mocks.transition).toHaveBeenCalledWith({ + connectionId: "conn-new", + event: "verify.failed_non_auth", + reason: "verify_failed", + ownerId: "owner-1", + }) + expect(result).toBeDefined() + }) + + it("passes config fields outside the provider's configFields through to the satellite insert unvalidated", async () => { + await connectionService.connectFromCredentials({ + workspaceId: "ws-1", + provider: "claude", + config: { apiKey: "sk-live", model: "claude-3", temperature: 0.7 }, + }) + + expect(mocks.insertRow).toHaveBeenCalledWith( + expect.objectContaining({ + config: { model: "claude-3", temperature: 0.7 }, + }), + "tx", + ) + }) + + it("does not resolve an ownerId for a kind:integration provider", async () => { + mockAdapter.provider.kind = "integration" + await connectionService.connectFromCredentials({ + workspaceId: "ws-1", + provider: "claude", + config: { apiKey: "sk-live" }, + }) + expect(mocks.findOwnerUserIdByWorkspaceId).not.toHaveBeenCalled() + expect(mocks.transition).toHaveBeenCalledWith( + expect.objectContaining({ ownerId: undefined }), + ) + }) + + it("revives a previously disconnected connection instead of inserting a duplicate", async () => { + mocks.findByProviderSourceId.mockResolvedValue({ + id: "conn-existing", + status: "disconnected", + }) + + const result = await connectionService.connectFromCredentials({ + workspaceId: "ws-1", + provider: "claude", + config: { apiKey: "sk-live" }, + }) + + expect(mocks.insert).not.toHaveBeenCalled() + expect(mocks.update).toHaveBeenCalledWith( + expect.objectContaining({ + id: "conn-existing", + values: expect.objectContaining({ integrationId: "int-1" }), + }), + "tx", + ) + expect(mocks.transition).toHaveBeenCalledWith({ + connectionId: "conn-existing", + event: "connect.completed", + ownerId: "owner-1", + tx: "tx", + }) + expect(result.id).toBe("conn-existing") + }) + + it("revives a keep_row provider's still-live satellite row via saveAuthByForeignKey instead of re-inserting it", async () => { + mockAdapter.store.onDisconnect = "keep_row" + mocks.findByProviderSourceId.mockResolvedValue({ + id: "conn-existing", + status: "disconnected", + integrationId: "int-existing", + }) + + const result = await connectionService.connectFromCredentials({ + workspaceId: "ws-1", + provider: "claude", + config: { apiKey: "sk-live" }, + }) + + expect(mocks.insertRow).not.toHaveBeenCalled() + expect(mocks.saveAuthByForeignKey).toHaveBeenCalledWith( + "int-existing", + expect.objectContaining({ authType: "secretText" }), + "tx", + ) + expect(mocks.update).toHaveBeenCalledWith( + expect.objectContaining({ + id: "conn-existing", + values: expect.objectContaining({ integrationId: "int-existing" }), + }), + "tx", + ) + expect(mocks.transition).toHaveBeenCalledWith({ + connectionId: "conn-existing", + event: "connect.completed", + ownerId: "owner-1", + tx: "tx", + }) + expect(result.id).toBe("conn-existing") + + mockAdapter.store.onDisconnect = "delete_row" + }) +}) + +describe("ConnectionService.startSession", () => { + it("throws connectionNotOAuth when the provider has no authorizeUrl", async () => { + mockAdapter.provider.authorizeUrl = undefined as never + await expect( + connectionService.startSession({ + workspaceId: "ws-1", + provider: "messenger", + purpose: "connect", + credential: {}, + callbackUrl: "https://app.example.test/integrations/messenger/callback", + }), + ).rejects.toMatchObject({ code: "connectionNotOAuth" }) + mockAdapter.provider.authorizeUrl = mocks.authorizeUrl + }) + + it("creates a session, builds state as sessionId.nonce, and persists the resulting nextAction", async () => { + const result = await connectionService.startSession({ + workspaceId: "ws-1", + provider: "messenger", + purpose: "connect", + credential: { clientId: "app-1" }, + callbackUrl: "https://app.example.test/integrations/messenger/callback", + actorUserId: "user-1", + }) + + expect(mocks.createSession).toHaveBeenCalledWith( + expect.objectContaining({ + workspaceId: "ws-1", + provider: "messenger", + purpose: "connect", + actorUserId: "user-1", + }), + ) + expect(mocks.authorizeUrl).toHaveBeenCalledWith({ + credential: { clientId: "app-1" }, + callbackUrl: "https://app.example.test/integrations/messenger/callback", + state: "session-1.nonce-abc", + }) + expect(mocks.submitInput).toHaveBeenCalledWith({ + id: "session-1", + nextAction: { + type: "open_url", + url: "https://provider.example.com/authorize", + }, + }) + expect(result.nextAction).toEqual({ + type: "open_url", + url: "https://provider.example.com/authorize", + }) + }) +}) + +describe("ConnectionService.completeAuthorization", () => { + it("throws connectionStateMismatch when the nonce does not resolve to the claimed sessionId", async () => { + mocks.findByNonce.mockResolvedValue({ + id: "session-other", + status: "pending", + provider: "messenger", + workspaceId: "ws-1", + }) + await expect( + connectionService.completeAuthorization({ + sessionId: "session-1", + nonce: "nonce-abc", + code: "auth-code", + callbackUrl: "https://app.example.test/callback", + credential: {}, + }), + ).rejects.toMatchObject({ code: "connectionStateMismatch" }) + }) + + it("throws connectionStateMismatch when the nonce resolves to nothing (forged/expired state)", async () => { + mocks.findByNonce.mockResolvedValue(undefined) + await expect( + connectionService.completeAuthorization({ + sessionId: "session-1", + nonce: "wrong-nonce", + code: "auth-code", + callbackUrl: "https://app.example.test/callback", + credential: {}, + }), + ).rejects.toMatchObject({ code: "connectionStateMismatch" }) + }) + + it("throws connectSessionExpired when the session is no longer pending", async () => { + mocks.findByNonce.mockResolvedValue({ + id: "session-1", + status: "completed", + provider: "messenger", + workspaceId: "ws-1", + }) + await expect( + connectionService.completeAuthorization({ + sessionId: "session-1", + nonce: "nonce-abc", + code: "auth-code", + callbackUrl: "https://app.example.test/callback", + credential: {}, + }), + ).rejects.toMatchObject({ code: "connectSessionExpired" }) + }) + + it("fails the session with provider_denied when exchangeCode throws", async () => { + mocks.exchangeCode.mockRejectedValue(new Error("bad code")) + await expect( + connectionService.completeAuthorization({ + sessionId: "session-1", + nonce: "nonce-abc", + code: "auth-code", + callbackUrl: "https://app.example.test/callback", + credential: {}, + }), + ).rejects.toMatchObject({ code: "connectionCredentialsRejected" }) + expect(mocks.failSession).toHaveBeenCalledWith({ + id: "session-1", + errorCode: "provider_denied", + }) + }) + + it("fails the session with no_candidates when listCandidates returns an empty list", async () => { + mocks.listCandidates.mockResolvedValue([]) + await expect( + connectionService.completeAuthorization({ + sessionId: "session-1", + nonce: "nonce-abc", + code: "auth-code", + callbackUrl: "https://app.example.test/callback", + credential: {}, + }), + ).rejects.toMatchObject({ code: "connectionNoCandidates" }) + expect(mocks.failSession).toHaveBeenCalledWith({ + id: "session-1", + errorCode: "no_candidates", + }) + }) + + it("marks a candidate not selectable when it is already connected in this workspace", async () => { + mocks.findByProviderAndSourceIdAnyWorkspace.mockResolvedValue({ + workspaceId: "ws-1", + status: "connected", + }) + await connectionService.completeAuthorization({ + sessionId: "session-1", + nonce: "nonce-abc", + code: "auth-code", + callbackUrl: "https://app.example.test/callback", + credential: {}, + }) + expect(mocks.attachAuthorization).toHaveBeenCalledWith( + expect.objectContaining({ + targets: [ + expect.objectContaining({ + id: "page-1", + selectable: false, + alreadyConnected: "this_workspace", + }), + ], + }), + ) + }) + + it("marks a candidate not selectable as other_workspace when connected elsewhere", async () => { + mocks.findByProviderAndSourceIdAnyWorkspace.mockResolvedValue({ + workspaceId: "ws-2", + status: "connected", + }) + await connectionService.completeAuthorization({ + sessionId: "session-1", + nonce: "nonce-abc", + code: "auth-code", + callbackUrl: "https://app.example.test/callback", + credential: {}, + }) + expect(mocks.attachAuthorization).toHaveBeenCalledWith( + expect.objectContaining({ + targets: [ + expect.objectContaining({ + id: "page-1", + selectable: false, + alreadyConnected: "other_workspace", + }), + ], + }), + ) + }) + + it("encrypts the exchanged auth and attaches selectable targets on success", async () => { + const result = await connectionService.completeAuthorization({ + sessionId: "session-1", + nonce: "nonce-abc", + code: "auth-code", + callbackUrl: "https://app.example.test/callback", + credential: {}, + }) + + expect(mocks.encryptObject).toHaveBeenCalledWith( + [ + expect.objectContaining({ + sourceId: "page-1", + displayName: "Page One", + }), + ], + "connect-session:session-1", + ) + expect(mocks.attachAuthorization).toHaveBeenCalledWith({ + id: "session-1", + encryptedAuth: { iv: "iv", ciphertext: "c", keyId: "k" }, + targets: [ + expect.objectContaining({ + id: "page-1", + name: "Page One", + selectable: true, + }), + ], + }) + expect(result.status).toBe("awaiting_selection") + }) + + it("falls back to describe() when the provider has no listCandidates", async () => { + mockAdapter.provider.listCandidates = undefined as never + await connectionService.completeAuthorization({ + sessionId: "session-1", + nonce: "nonce-abc", + code: "auth-code", + callbackUrl: "https://app.example.test/callback", + credential: {}, + }) + expect(mocks.attachAuthorization).toHaveBeenCalledWith( + expect.objectContaining({ + targets: [ + expect.objectContaining({ id: "workspace", name: "Test Provider" }), + ], + }), + ) + mockAdapter.provider.listCandidates = mocks.listCandidates + }) +}) + +describe("ConnectionService.reconnect", () => { + it("throws notFound when the connection does not belong to the workspace", async () => { + mocks.findByIdForWorkspace.mockResolvedValue(undefined) + await expect( + connectionService.reconnect({ + connectionId: "conn-1", + workspaceId: "ws-1", + credential: {}, + callbackUrl: "https://app.example.test/callback", + }), + ).rejects.toThrow("Connection not found") + }) + + it("starts a session with purpose reconnect and targetConnectionId set to the existing connection", async () => { + mocks.findByIdForWorkspace.mockResolvedValue(baseConnection()) + await connectionService.reconnect({ + connectionId: "conn-1", + workspaceId: "ws-1", + credential: { clientId: "app-1" }, + callbackUrl: "https://app.example.test/callback", + actorUserId: "user-1", + }) + expect(mocks.createSession).toHaveBeenCalledWith( + expect.objectContaining({ + workspaceId: "ws-1", + provider: "messenger", + purpose: "reconnect", + targetConnectionId: "conn-1", + actorUserId: "user-1", + }), + ) + }) +}) + +describe("ConnectionService.completeAuthorization (reconnect path)", () => { + const reconnectSession = { + id: "session-1", + workspaceId: "ws-1", + provider: "messenger", + purpose: "reconnect", + targetConnectionId: "conn-1", + status: "pending", + } + + it("verifies identity, saves auth, and revives the connection via connect.completed (not auth.saved)", async () => { + mocks.findByNonce.mockResolvedValue(reconnectSession) + mocks.findById.mockResolvedValue(baseConnection({ sourceId: "page-1" })) + mocks.exchangeCode.mockResolvedValue({ + authType: "oauth2", + clientId: "id", + clientSecret: "secret", + redirectUrl: "https://x", + tokens: { accessToken: "tok", expiresAt: "2030-01-01T00:00:00.000Z" }, + }) + mockAdapter.provider.describe = () => ({ + sourceId: "page-1", + displayName: "Page One", + }) + + const result = await connectionService.completeAuthorization({ + sessionId: "session-1", + nonce: "nonce-abc", + code: "auth-code", + callbackUrl: "https://app.example.test/callback", + credential: {}, + }) + + expect(mocks.saveAuthByForeignKey).toHaveBeenCalledWith( + "inbox-1", + expect.objectContaining({ authType: "oauth2" }), + "tx", + ) + expect(mocks.update).toHaveBeenCalledWith( + expect.objectContaining({ + id: "conn-1", + values: expect.objectContaining({ + authExpiresAt: new Date("2030-01-01T00:00:00.000Z"), + lastError: null, + }), + }), + "tx", + ) + expect(mocks.transition).toHaveBeenCalledWith({ + connectionId: "conn-1", + event: "connect.completed", + ownerId: "owner-1", + tx: "tx", + }) + expect(mocks.recordAuthSaved).not.toHaveBeenCalled() + expect(mocks.recordResults).toHaveBeenCalledWith({ + id: "session-1", + results: [ + { targetId: "page-1", status: "connected", connectionId: "conn-1" }, + ], + resultConnectionIds: ["conn-1"], + }) + expect(result.status).toBe("completed") + }) + + it("throws connectionIdentityMismatch and fails the session when the re-granted account differs", async () => { + mocks.findByNonce.mockResolvedValue(reconnectSession) + mocks.findById.mockResolvedValue(baseConnection({ sourceId: "page-1" })) + mockAdapter.provider.describe = () => ({ + sourceId: "page-2", + displayName: "A Different Page", + }) + + await expect( + connectionService.completeAuthorization({ + sessionId: "session-1", + nonce: "nonce-abc", + code: "auth-code", + callbackUrl: "https://app.example.test/callback", + credential: {}, + }), + ).rejects.toMatchObject({ code: "connectionIdentityMismatch" }) + expect(mocks.failSession).toHaveBeenCalledWith({ + id: "session-1", + errorCode: "provider_denied", + }) + expect(mocks.saveAuthByForeignKey).not.toHaveBeenCalled() + + mockAdapter.provider.describe = () => ({ + sourceId: "workspace", + displayName: "Test Provider", + }) + }) + + it("throws notFound and fails the session when the target connection no longer exists", async () => { + mocks.findByNonce.mockResolvedValue(reconnectSession) + mocks.findById.mockResolvedValue(undefined) + + await expect( + connectionService.completeAuthorization({ + sessionId: "session-1", + nonce: "nonce-abc", + code: "auth-code", + callbackUrl: "https://app.example.test/callback", + credential: {}, + }), + ).rejects.toMatchObject({ code: "notFound" }) + expect(mocks.failSession).toHaveBeenCalledWith({ + id: "session-1", + errorCode: "internal_error", + }) + }) + + it("never resolves an ownerId for a kind:integration connection being reconnected", async () => { + mocks.findByNonce.mockResolvedValue(reconnectSession) + mocks.findById.mockResolvedValue( + baseConnection({ + sourceId: "page-1", + kind: "integration", + inboxId: null, + integrationId: "int-1", + }), + ) + mockAdapter.provider.describe = () => ({ + sourceId: "page-1", + displayName: "Page One", + }) + + await connectionService.completeAuthorization({ + sessionId: "session-1", + nonce: "nonce-abc", + code: "auth-code", + callbackUrl: "https://app.example.test/callback", + credential: {}, + }) + + expect(mocks.findOwnerUserIdByWorkspaceId).not.toHaveBeenCalled() + expect(mocks.transition).toHaveBeenCalledWith( + expect.objectContaining({ ownerId: undefined }), + ) + + mockAdapter.provider.describe = () => ({ + sourceId: "workspace", + displayName: "Test Provider", + }) + }) +}) + +describe("ConnectionService.connectTargets", () => { + const awaitingSession = { + id: "session-1", + workspaceId: "ws-1", + provider: "messenger", + status: "awaiting_selection", + encryptedAuth: { iv: "iv", ciphertext: "c", keyId: "k" }, + targets: [ + { id: "page-1", name: "Page One", selectable: true }, + { + id: "page-2", + name: "Page Two", + selectable: false, + alreadyConnected: "other_workspace", + }, + ], + } + + beforeEach(() => { + mocks.findSessionByIdForWorkspace.mockResolvedValue(awaitingSession) + mocks.decryptObject.mockResolvedValue([ + { + sourceId: "page-1", + displayName: "Page One", + auth: { authType: "none" }, + }, + { + sourceId: "page-2", + displayName: "Page Two", + auth: { authType: "none" }, + }, + ]) + mocks.claimTarget.mockResolvedValue(true) + mocks.recordResults.mockImplementation(async (input) => ({ + ...awaitingSession, + status: "completed", + results: input.results, + resultConnectionIds: input.resultConnectionIds, + })) + // Real `describe()` derives the descriptor from `auth`, not from the + // candidate's own redacted `sourceId`/`displayName` — the shared + // `mockAdapter` default ignores its input, so tests that need a + // specific descriptor stand one up per candidate here. + mockAdapter.provider.describe = () => ({ + sourceId: "page-1", + displayName: "Page One", + }) + }) + + afterEach(() => { + mockAdapter.provider.describe = () => ({ + sourceId: "workspace", + displayName: "Test Provider", + }) + }) + + it("throws connectSessionExpired when the session is not awaiting_selection", async () => { + mocks.findSessionByIdForWorkspace.mockResolvedValue({ + ...awaitingSession, + status: "pending", + }) + await expect( + connectionService.connectTargets({ + sessionId: "session-1", + workspaceId: "ws-1", + targetIds: ["page-1"], + }), + ).rejects.toMatchObject({ code: "connectSessionExpired" }) + }) + + it("connects a selectable target: creates the inbox with skipQuota, inserts the Connection, and reports connected", async () => { + const result = await connectionService.connectTargets({ + sessionId: "session-1", + workspaceId: "ws-1", + targetIds: ["page-1"], + }) + + expect(mocks.claimTarget).toHaveBeenCalledWith({ + id: "session-1", + targetId: "page-1", + }) + expect(mocks.inboxCreate).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + workspaceId: "ws-1", + channel: "messenger", + sourceId: "page-1", + }), + ownerId: "owner-1", + skipQuota: true, + }), + ) + expect(mocks.insertRow).toHaveBeenCalledWith( + expect.objectContaining({ inboxId: "inbox-new" }), + "tx", + ) + expect(mocks.transition).toHaveBeenCalledWith( + expect.objectContaining({ + event: "connect.completed", + ownerId: "owner-1", + }), + ) + expect(result.outcomes).toEqual([ + { targetId: "page-1", status: "connected", connectionId: "conn-new" }, + ]) + expect(result.connections).toHaveLength(1) + expect(mocks.recordResults).toHaveBeenCalledWith({ + id: "session-1", + results: result.outcomes, + resultConnectionIds: ["conn-new"], + }) + }) + + it("subscribes the provider webhook for the newly connected target", async () => { + await connectionService.connectTargets({ + sessionId: "session-1", + workspaceId: "ws-1", + targetIds: ["page-1"], + }) + + expect(mocks.subscribe).toHaveBeenCalledWith({ + auth: { authType: "none" }, + }) + }) + + it("maps a non-selectable already-connected target to duplicated without claiming it", async () => { + const result = await connectionService.connectTargets({ + sessionId: "session-1", + workspaceId: "ws-1", + targetIds: ["page-2"], + }) + expect(mocks.claimTarget).not.toHaveBeenCalled() + expect(result.outcomes).toEqual([ + { targetId: "page-2", status: "duplicated", reason: "alreadyConnected" }, + ]) + }) + + it("maps a claim race (already claimed by a prior call) to duplicated without connecting twice", async () => { + mocks.claimTarget.mockResolvedValue(false) + const result = await connectionService.connectTargets({ + sessionId: "session-1", + workspaceId: "ws-1", + targetIds: ["page-1"], + }) + expect(mocks.insertRow).not.toHaveBeenCalled() + expect(result.outcomes).toEqual([ + { targetId: "page-1", status: "duplicated", reason: "alreadyConnected" }, + ]) + }) + + it("maps a channelLimitReached failure from connectCandidate to a limitReached outcome", async () => { + mocks.findOwnerUserIdByWorkspaceId.mockResolvedValue("owner-1") + mocks.inboxCreate.mockRejectedValue(channelLimitReachedException()) + const result = await connectionService.connectTargets({ + sessionId: "session-1", + workspaceId: "ws-1", + targetIds: ["page-1"], + }) + expect(result.outcomes).toEqual([ + { targetId: "page-1", status: "limitReached", reason: "workspaceLimit" }, + ]) + expect(result.connections).toHaveLength(0) + }) + + it("maps an unrecognized targetId to a failed/unknown outcome", async () => { + const result = await connectionService.connectTargets({ + sessionId: "session-1", + workspaceId: "ws-1", + targetIds: ["ghost-page"], + }) + expect(result.outcomes).toEqual([ + { targetId: "ghost-page", status: "failed", reason: "unknown" }, + ]) + }) + + it("revives a keep_row provider's still-live satellite row instead of re-inserting it", async () => { + mockAdapter.store.onDisconnect = "keep_row" + mocks.inboxCreate.mockResolvedValue({ + inbox: { id: "inbox-new" }, + wasCreated: true, + }) + mocks.findByProviderSourceId.mockResolvedValue({ + id: "conn-existing", + status: "disconnected", + inboxId: "inbox-existing", + }) + + const result = await connectionService.connectTargets({ + sessionId: "session-1", + workspaceId: "ws-1", + targetIds: ["page-1"], + }) + + expect(mocks.insertRow).not.toHaveBeenCalled() + expect(mocks.saveAuthByForeignKey).toHaveBeenCalledWith( + "inbox-existing", + { authType: "none" }, + "tx", + ) + expect(result.outcomes).toEqual([ + { + targetId: "page-1", + status: "connected", + connectionId: "conn-existing", + }, + ]) + + mockAdapter.store.onDisconnect = "delete_row" + }) +}) diff --git a/packages/connections/package.json b/packages/connections/package.json new file mode 100644 index 0000000000..b445815ea0 --- /dev/null +++ b/packages/connections/package.json @@ -0,0 +1,51 @@ +{ + "name": "@chatbotx.io/connections", + "version": "0.1.0", + "private": true, + "type": "module", + "dependencies": { + "@chatbotx.io/business": "workspace:*", + "@chatbotx.io/database": "workspace:*", + "@chatbotx.io/encryption": "workspace:*", + "@chatbotx.io/integration-active-campaign": "workspace:*", + "@chatbotx.io/integration-api": "workspace:*", + "@chatbotx.io/integration-chatbotx": "workspace:*", + "@chatbotx.io/integration-drip": "workspace:*", + "@chatbotx.io/integration-facebook-ads": "workspace:*", + "@chatbotx.io/integration-get-response": "workspace:*", + "@chatbotx.io/integration-google-calendar": "workspace:*", + "@chatbotx.io/integration-google-sheets": "workspace:*", + "@chatbotx.io/integration-instagram": "workspace:*", + "@chatbotx.io/integration-instagram-facebook": "workspace:*", + "@chatbotx.io/integration-klaviyo": "workspace:*", + "@chatbotx.io/integration-mailchimp": "workspace:*", + "@chatbotx.io/integration-mailer-lite": "workspace:*", + "@chatbotx.io/integration-messenger": "workspace:*", + "@chatbotx.io/integration-moosend": "workspace:*", + "@chatbotx.io/integration-sendgrid": "workspace:*", + "@chatbotx.io/integration-smtp": "workspace:*", + "@chatbotx.io/integration-telegram": "workspace:*", + "@chatbotx.io/integration-tiktok": "workspace:*", + "@chatbotx.io/integration-webchat": "workspace:*", + "@chatbotx.io/integration-whatsapp": "workspace:*", + "@chatbotx.io/integration-zalo": "workspace:*", + "@chatbotx.io/logger": "workspace:*", + "@chatbotx.io/sdk": "workspace:*", + "@chatbotx.io/utils": "workspace:*" + }, + "devDependencies": { + "@chatbotx.io/typescript-config": "workspace:*", + "@chatbotx.io/vitest-config": "workspace:*", + "@types/node": "^24.10.4", + "typescript": "^5", + "vitest": "^4.1.0" + }, + "exports": { + ".": "./src/index.ts" + }, + "scripts": { + "check-types": "tsc --noEmit", + "test": "vitest run --passWithNoTests", + "test:watch": "vitest" + } +} diff --git a/packages/connections/src/index.ts b/packages/connections/src/index.ts new file mode 100644 index 0000000000..4dbf86902d --- /dev/null +++ b/packages/connections/src/index.ts @@ -0,0 +1,2 @@ +export * from "./registry" +export * from "./service" diff --git a/packages/connections/src/logger.ts b/packages/connections/src/logger.ts new file mode 100644 index 0000000000..532338bfcc --- /dev/null +++ b/packages/connections/src/logger.ts @@ -0,0 +1,3 @@ +import { getChildLogger } from "@chatbotx.io/logger" + +export const logger = getChildLogger("Connections") diff --git a/packages/connections/src/registry.ts b/packages/connections/src/registry.ts new file mode 100644 index 0000000000..cfa8afb22c --- /dev/null +++ b/packages/connections/src/registry.ts @@ -0,0 +1,142 @@ +import { + CONNECTION_STORE_BINDINGS, + type ConnectionAdapter, + type ConnectionRegistry, + claudeConnectionProvider, + deepseekConnectionProvider, + geminiConnectionProvider, + openaiCompatibleConnectionProvider, + openaiConnectionProvider, + openrouterConnectionProvider, +} from "@chatbotx.io/business/connection" +import { integration as integrationActiveCampaign } from "@chatbotx.io/integration-active-campaign" +import { integration as integrationApi } from "@chatbotx.io/integration-api" +import { integration as integrationChatbotx } from "@chatbotx.io/integration-chatbotx" +import { integration as integrationDrip } from "@chatbotx.io/integration-drip" +import { integration as integrationFacebookAds } from "@chatbotx.io/integration-facebook-ads" +import { integration as integrationGetResponse } from "@chatbotx.io/integration-get-response" +import { integration as integrationGoogleCalendar } from "@chatbotx.io/integration-google-calendar" +import { integration as integrationGoogleSheets } from "@chatbotx.io/integration-google-sheets" +import { integration as integrationInstagram } from "@chatbotx.io/integration-instagram" +import { integration as integrationInstagramFacebook } from "@chatbotx.io/integration-instagram-facebook" +import { integration as integrationKlaviyo } from "@chatbotx.io/integration-klaviyo" +import { integration as integrationMailchimp } from "@chatbotx.io/integration-mailchimp" +import { integration as integrationMailerLite } from "@chatbotx.io/integration-mailer-lite" +import { integration as integrationMessenger } from "@chatbotx.io/integration-messenger" +import { integration as integrationMoosend } from "@chatbotx.io/integration-moosend" +import { integration as integrationSendGrid } from "@chatbotx.io/integration-sendgrid" +import { integration as integrationSmtp } from "@chatbotx.io/integration-smtp" +import { integration as integrationTelegram } from "@chatbotx.io/integration-telegram" +import { integration as integrationTiktok } from "@chatbotx.io/integration-tiktok" +import { integration as integrationWebchat } from "@chatbotx.io/integration-webchat" +import { integration as integrationWhatsapp } from "@chatbotx.io/integration-whatsapp" +import { integration as integrationZalo } from "@chatbotx.io/integration-zalo" +import type { Integration, IntegrationDefinition } from "@chatbotx.io/sdk" + +/** + * Builds a `ConnectionAdapter` from an `integrations/` SDK package's + * `Integration` instance. Throws at module-load time (not per-request) if + * the provider's `connection` block is missing — every `IntegrationType` + * wired into this registry is asserted, at registration time, to have one; + * a provider that regresses to having none fails the build immediately + * instead of silently returning `undefined` behind an optional chain. + */ +const fromIntegration = ( + // biome-ignore lint/suspicious/noExplicitAny: heterogeneous registry + integration: Integration>, + storeKey: keyof typeof CONNECTION_STORE_BINDINGS, + credentialType?: ConnectionAdapter["credentialType"], +): ConnectionAdapter => { + const provider = integration.connection + if (!provider) { + throw new Error( + `@chatbotx.io/connections: "${integration.name}" has no connection block registered under CONNECTION_REGISTRY.${storeKey}`, + ) + } + // `null` is legitimate here (chatbotx has no satellite table — the Inbox + // row itself is the whole connection); only a missing `provider` above is + // treated as a build-time error. + const store = CONNECTION_STORE_BINDINGS[storeKey] ?? undefined + return { integration, provider, store, credentialType } +} + +/** + * A credential-only provider (no `integrations/` package) paired with its + * store binding — the same shape `fromIntegration` produces, minus the SDK + * `Integration` wrapper these providers never had. + */ +const fromCredentialProvider = ( + provider: ConnectionAdapter["provider"], + storeKey: keyof typeof CONNECTION_STORE_BINDINGS, +): ConnectionAdapter => { + const store = CONNECTION_STORE_BINDINGS[storeKey] + if (!store) { + throw new Error( + `@chatbotx.io/connections: no store binding for "${storeKey}"`, + ) + } + return { provider, store } +} + +/** + * Compile-time exhaustive `Record`. + * `null` marks a type with no connect lifecycle yet: `metaCatalog` and + * `outlookCalendar` have DB tables but no `integrations/` package or live + * builder feature yet. `chatbotx` DOES have a real adapter (its `store` is + * `undefined` — it has no satellite table; the `Inbox` row is the whole + * connection). + */ +export const CONNECTION_REGISTRY: ConnectionRegistry = { + activeCampaign: fromIntegration(integrationActiveCampaign, "activeCampaign"), + api: fromIntegration(integrationApi, "api"), + chatbotx: fromIntegration(integrationChatbotx, "chatbotx"), + claude: fromCredentialProvider(claudeConnectionProvider, "claude"), + deepseek: fromCredentialProvider(deepseekConnectionProvider, "deepseek"), + drip: fromIntegration(integrationDrip, "drip"), + facebookAds: fromIntegration( + integrationFacebookAds, + "facebookAds", + "messenger", + ), + gemini: fromCredentialProvider(geminiConnectionProvider, "gemini"), + getResponse: fromIntegration(integrationGetResponse, "getResponse"), + googleCalendar: fromIntegration( + integrationGoogleCalendar, + "googleCalendar", + "google", + ), + googleSheets: fromIntegration( + integrationGoogleSheets, + "googleSheets", + "google", + ), + instagram: fromIntegration(integrationInstagram, "instagram", "instagram"), + instagramFacebook: fromIntegration( + integrationInstagramFacebook, + "instagramFacebook", + "instagramFacebook", + ), + klaviyo: fromIntegration(integrationKlaviyo, "klaviyo"), + mailchimp: fromIntegration(integrationMailchimp, "mailchimp"), + mailerLite: fromIntegration(integrationMailerLite, "mailerLite"), + messenger: fromIntegration(integrationMessenger, "messenger", "messenger"), + metaCatalog: null, + moosend: fromIntegration(integrationMoosend, "moosend"), + openai: fromCredentialProvider(openaiConnectionProvider, "openai"), + openaiCompatible: fromCredentialProvider( + openaiCompatibleConnectionProvider, + "openaiCompatible", + ), + openrouter: fromCredentialProvider( + openrouterConnectionProvider, + "openrouter", + ), + outlookCalendar: null, + sendGrid: fromIntegration(integrationSendGrid, "sendGrid"), + smtp: fromIntegration(integrationSmtp, "smtp"), + telegram: fromIntegration(integrationTelegram, "telegram"), + tiktok: fromIntegration(integrationTiktok, "tiktok", "tiktok"), + webchat: fromIntegration(integrationWebchat, "webchat"), + whatsapp: fromIntegration(integrationWhatsapp, "whatsapp", "whatsapp"), + zalo: fromIntegration(integrationZalo, "zalo", "zalo"), +} diff --git a/packages/connections/src/service.ts b/packages/connections/src/service.ts new file mode 100644 index 0000000000..30992f9b8a --- /dev/null +++ b/packages/connections/src/service.ts @@ -0,0 +1,1224 @@ +import { inboxService, workspaceMemberService } from "@chatbotx.io/business" +import { connectSessionService } from "@chatbotx.io/business/connect-session" +import { + type ConnectionAdapter, + connectionStateService, + isActiveConnectionStatus, +} from "@chatbotx.io/business/connection" +import { + ChatbotXException, + connectionAlreadyConnectedException, + connectionCredentialsRejectedException, + connectionIdentityMismatchException, + connectionInactiveException, + connectionNoCandidatesException, + connectionNotConfiguredException, + connectionNotOAuthException, + connectionNotRefreshableException, + connectionStateMismatchException, + connectionWrongStrategyException, + connectSessionExpiredException, + notFoundException, + toPublicErrorMessage, + validationException, +} from "@chatbotx.io/business/errors" +import { db, isUniqueViolationError } from "@chatbotx.io/database/client" +import type { + ChannelType, + ConnectSessionOutcome, + ConnectSessionPurpose, + IntegrationType, +} from "@chatbotx.io/database/partials" +import { connectionRepository } from "@chatbotx.io/database/repositories" +import type { + ConnectionModel, + ConnectSessionModel, +} from "@chatbotx.io/database/types" +import { encryptUtils } from "@chatbotx.io/encryption" +import type { + AuthStore, + AuthValue, + ConnectionCandidate, + ConnectionConfigField, + ConnectionCredential, + ConnectNextAction, +} from "@chatbotx.io/sdk" +import { z } from "zod" +import { logger } from "./logger" +import { CONNECTION_REGISTRY } from "./registry" + +/** + * What a session's `encryptedAuth` blob actually holds once + * `completeAuthorization` lists candidates: the full `ConnectionCandidate[]` + * (each with its own `auth`), not just the single exchanged `auth` — a + * multi-account provider (Messenger) hands back one distinct per-page auth + * per candidate. `auth` itself is provider-specific and unvalidated here + * (same trust boundary as `asAuthValue` in `store-bindings.ts`); everything + * else mirrors `ConnectSessionTarget`. + */ +const encryptedCandidatesSchema = z.array( + z.object({ + sourceId: z.string(), + displayName: z.string(), + authExpiresAt: z.string().optional(), + avatarUrl: z.string().optional(), + alreadyConnected: z.enum(["this_workspace", "other_workspace"]).optional(), + auth: z.unknown(), + }), +) + +/** + * The FK a `Connection` row actually carries to its satellite row — + * `inboxId` for channels, `integrationId` for workspace integrations. `null` + * only for `chatbotx` (no satellite table at all). + */ +const resolveForeignKey = (connection: ConnectionModel): string | null => + connection.inboxId ?? connection.integrationId ?? null + +/** + * Validates a raw `config` object (a credential-strategy `connect` request + * body) against a provider's `configFields` declaration, coercing each + * value to its declared type. Throws the same `validationException` shape + * every other service uses, field-scoped, so a public/private API caller + * can attach it to the right form field without special-casing this path. + */ +const parseConfig = ( + configFields: readonly ConnectionConfigField[], + rawConfig: Record, +): Record => { + const parsed: Record = {} + for (const field of configFields) { + const value = rawConfig[field.name] + if (value === undefined || value === null || value === "") { + if (field.required) { + throw validationException(field.name, `${field.name} is required`) + } + continue + } + switch (field.type) { + case "string": + case "secret": + case "url": + if (typeof value !== "string") { + throw validationException( + field.name, + `${field.name} must be a string`, + ) + } + parsed[field.name] = value + break + case "number": { + const num = typeof value === "number" ? value : Number(value) + if (Number.isNaN(num)) { + throw validationException( + field.name, + `${field.name} must be a number`, + ) + } + parsed[field.name] = num + break + } + case "boolean": + if (typeof value !== "boolean") { + throw validationException( + field.name, + `${field.name} must be a boolean`, + ) + } + parsed[field.name] = value + break + case "enum": + if (typeof value !== "string" || !field.enumValues?.includes(value)) { + throw validationException( + field.name, + `${field.name} must be one of ${(field.enumValues ?? []).join(", ")}`, + ) + } + parsed[field.name] = value + break + default: { + const exhaustive: never = field.type + throw new Error(`Unhandled connection config field type: ${exhaustive}`) + } + } + } + return parsed +} + +/** + * Registry-aware orchestration over the `Connection` domain: provider-side + * teardown/refresh/health-check calls plus the store-binding CRUD, delegating + * every status write to the registry-free `connectionStateService`. Lives in + * `@chatbotx.io/connections` (not `@chatbotx.io/business`) because it needs + * the full `CONNECTION_REGISTRY`, which itself depends on `business` — see + * the placement note on `connectionStateService`. + */ +class ConnectionService { + private resolveAdapter(provider: string): ConnectionAdapter { + const adapter = + CONNECTION_REGISTRY[provider as keyof typeof CONNECTION_REGISTRY] + if (!adapter) { + throw connectionNotConfiguredException(provider) + } + return adapter + } + + private async findOrThrow(input: { + connectionId: string + workspaceId: string + }): Promise { + const connection = await connectionRepository.findByIdForWorkspace({ + id: input.connectionId, + workspaceId: input.workspaceId, + }) + if (!connection) { + throw notFoundException("Connection not found") + } + return connection + } + + /** + * Best-effort `provider.webhook.subscribe` right after a fresh + * `connect.completed` — the FSM design's own "webhook.subscribe + * best-effort (failure → degraded, lastError)" edge (`state.ts`'s + * `verify.failed_non_auth`), mirrored from `disconnect`'s existing + * best-effort `webhook.unsubscribe` call. Never throws: a subscribe + * failure degrades the just-created connection instead of failing the + * whole connect (the row and quota consumption already committed). + */ + private async subscribeWebhookBestEffort(input: { + adapter: ConnectionAdapter + auth: AuthValue + connection: ConnectionModel + ownerId: string | undefined + }): Promise { + if (!input.adapter.provider.webhook) { + return input.connection + } + try { + await input.adapter.provider.webhook.subscribe({ auth: input.auth }) + return input.connection + } catch (err) { + logger.warn( + { + err, + connectionId: input.connection.id, + provider: input.connection.provider, + }, + "connect: webhook subscribe failed, marking connection degraded", + ) + return await connectionStateService.transition({ + connectionId: input.connection.id, + event: "verify.failed_non_auth", + reason: "verify_failed", + ownerId: input.ownerId, + }) + } + } + + /** + * Resolves the workspace-owner user id for the FSM's quota edge — + * `undefined` for anything but a `kind: "channel"` connection, since + * `ConnectionStateService.applyQuotaEdge` always targets the `"channels"` + * metric. Passing an owner for a workspace-integration connection (AI + * providers, marketing tools) would incorrectly consume/release a + * channel-quota slot when that row crosses the active/inactive boundary. + */ + private async resolveOwnerId( + connection: Pick, + ): Promise { + if (connection.kind !== "channel") { + return + } + return await workspaceMemberService.findOwnerUserIdByWorkspaceId({ + workspaceId: connection.workspaceId, + }) + } + + /** + * User-initiated teardown: best-effort provider-side disconnect + webhook + * unsubscribe (never blocks the local state transition on an upstream + * failure), then the satellite row's own `onDisconnect` policy + * (`delete_row`/`keep_row`), then the FSM transition. + * + * Scope note: this is the **generic** disconnect path shared by every + * provider. Bespoke per-provider teardown side effects that predate the + * Connection domain — messenger's shared-IG-page `general_info` + * preservation, WhatsApp's coexist/staging cleanup, TikTok/Zalo specifics — + * are NOT ported here; those remain in their existing per-channel disconnect + * actions until a dedicated follow-up audits each one individually. + */ + async disconnect(input: { + connectionId: string + workspaceId: string + }): Promise { + const connection = await this.findOrThrow(input) + const adapter = this.resolveAdapter(connection.provider) + const foreignKey = resolveForeignKey(connection) + + if (adapter.store && foreignKey) { + try { + const auth = await adapter.store.loadAuthByForeignKey(foreignKey) + if (adapter.integration) { + await adapter.integration.disconnect(auth) + } + if (adapter.provider.webhook) { + await adapter.provider.webhook.unsubscribe({ auth }) + } + } catch (err) { + logger.warn( + { err, connectionId: connection.id, provider: connection.provider }, + "connection disconnect: provider-side teardown failed, proceeding with local disconnect", + ) + } + await adapter.store.deleteRowByForeignKey(foreignKey) + } + + const ownerId = await this.resolveOwnerId(connection) + return await connectionStateService.transition({ + connectionId: connection.id, + event: "user.disconnect", + ownerId, + }) + } + + /** Forces `refreshAuth` regardless of expiry — `POST /v1/connections/{id}/refresh`. */ + async refresh(input: { + connectionId: string + workspaceId: string + }): Promise { + const connection = await this.findOrThrow(input) + if (!isActiveConnectionStatus(connection.status)) { + throw connectionInactiveException() + } + const adapter = this.resolveAdapter(connection.provider) + if (!adapter.integration?.refreshAuth) { + throw connectionNotRefreshableException(connection.provider) + } + if (!adapter.store) { + throw connectionNotConfiguredException(connection.provider) + } + const foreignKey = resolveForeignKey(connection) + if (!foreignKey) { + throw connectionNotConfiguredException(connection.provider) + } + const store = adapter.store + + const auth = await store.loadAuthByForeignKey(foreignKey) + const authStore: AuthStore = { + load: async () => await store.loadAuthByForeignKey(foreignKey), + save: async (newAuth) => { + await store.saveAuthByForeignKey(foreignKey, newAuth) + const authExpiresAt = + newAuth.authType === "oauth2" && newAuth.tokens.expiresAt + ? new Date(newAuth.tokens.expiresAt) + : null + await connectionStateService.recordAuthSaved({ + connectionId: connection.id, + authExpiresAt, + }) + }, + markOffline: async () => { + const ownerId = await this.resolveOwnerId(connection) + await connectionStateService.markUnhealthy({ + connectionId: connection.id, + ownerId, + }) + }, + } + + // `refreshAuth`/`ensureFreshAuth` only ever read `ctx.auth`/`ctx.authStore` + // (never `ctx.platform`/`ctx.storagePrefix`/`ctx.integrationDetail`) — + // see `Integration.refreshAndPersist` in `@chatbotx.io/sdk`. The + // `platform` stub below is structurally required but never invoked on + // this path. + await adapter.integration.ensureFreshAuth( + { + storagePrefix: "", + auth, + authStore, + platform: { + appUrl: "", + wsUrl: "", + storageUrl: "", + getRealtimeAuthHeaders: async () => ({}), + }, + }, + { force: true }, + ) + + const refreshed = await connectionRepository.findById({ + id: connection.id, + }) + if (!refreshed) { + throw notFoundException("Connection not found") + } + return refreshed + } + + /** Live health check without a refresh cycle — `POST /v1/connections/{id}/verify`. */ + async verify(input: { + connectionId: string + workspaceId: string + }): Promise { + const connection = await this.findOrThrow(input) + if (!isActiveConnectionStatus(connection.status)) { + throw connectionInactiveException() + } + const adapter = this.resolveAdapter(connection.provider) + if (!adapter.store) { + throw connectionNotConfiguredException(connection.provider) + } + const foreignKey = resolveForeignKey(connection) + if (!foreignKey) { + throw connectionNotConfiguredException(connection.provider) + } + + const auth = await adapter.store.loadAuthByForeignKey(foreignKey) + const health = await adapter.provider.verify({ auth }) + const ownerId = await this.resolveOwnerId(connection) + + if (health.ok) { + return await connectionStateService.transition({ + connectionId: connection.id, + event: "verify.ok", + ownerId, + }) + } + if (health.revoked) { + return await connectionStateService.markUnhealthy({ + connectionId: connection.id, + reason: "token_revoked", + ownerId, + }) + } + return await connectionStateService.transition({ + connectionId: connection.id, + event: "verify.failed_non_auth", + reason: "verify_failed", + ownerId, + }) + } + + /** + * `token`/`api_key`/`self_serve` connect: validates `config` against the + * provider's `configFields`, live-validates it via `fromCredentials`, then + * creates (or revives a previously disconnected) `Connection` row plus its + * satellite table row in one transaction. Today every credential-strategy + * provider is `kind: "integration"` (a workspace singleton, no quota + * edge), but the transition still runs through `connectionStateService` + * — not a hardcoded `status: "connected"` insert — so a future `kind: + * "channel"` credential-strategy provider consumes quota correctly too. + */ + async connectFromCredentials(input: { + workspaceId: string + provider: IntegrationType + config: Record + actorUserId?: string | null + /** + * Allows replacing an already-`connected`/`degraded` connection's auth + * and config in place instead of throwing `connectionAlreadyConnected` + * — only for backward-compat upsert aliases (the legacy `PUT + * /v1/integrations/ai/{provider}` route, which has always replaced the + * stored API key/config on repeat calls). The new `POST /v1/connections` + * surface must NOT set this — a fresh connect should reject an existing + * active connection. + */ + allowUpdate?: boolean + }): Promise { + const adapter = this.resolveAdapter(input.provider) + const { provider } = adapter + const isCredentialStrategy = + provider.strategy === "token" || + provider.strategy === "api_key" || + provider.strategy === "self_serve" + if (!(isCredentialStrategy && provider.fromCredentials)) { + throw connectionWrongStrategyException(input.provider) + } + if (!adapter.store) { + throw connectionNotConfiguredException(input.provider) + } + const store = adapter.store + + const parsedConfig = parseConfig(provider.configFields, input.config) + // Fields the caller sent that aren't part of the provider's own + // credential shape (`configFields`) — e.g. an AI provider's `model`/ + // `temperature`/`maxOutputTokens` — flow straight through to the + // satellite row's extra columns via `store.insertRow`'s `config`, + // unvalidated (the satellite table's own NOT NULL/type constraints are + // the validation for those). + const configFieldNames = new Set( + provider.configFields.map((field) => field.name), + ) + const extraConfig = Object.fromEntries( + Object.entries(input.config).filter( + ([key]) => !configFieldNames.has(key), + ), + ) + + let auth: AuthValue + try { + auth = await provider.fromCredentials(parsedConfig) + } catch (err) { + throw connectionCredentialsRejectedException( + toPublicErrorMessage(err, "The provided credentials were rejected."), + ) + } + + const descriptor = provider.describe(auth) + + const existing = await connectionRepository.findByProviderSourceId({ + workspaceId: input.workspaceId, + provider: input.provider, + sourceId: descriptor.sourceId, + }) + if ( + existing && + isActiveConnectionStatus(existing.status) && + !input.allowUpdate + ) { + throw connectionAlreadyConnectedException() + } + + const ownerId = await this.resolveOwnerId({ + kind: provider.kind, + workspaceId: input.workspaceId, + }) + + const connection = await db.transaction(async (tx) => { + // Revive: for a `keep_row` provider (e.g. Telegram, which connects + // via `connectFromCredentials`'s bot-token strategy) the satellite + // row was never deleted on disconnect — calling `store.insertRow` + // again would collide with its own unique constraint and get + // miscategorized as `connectionAlreadyConnectedException` below, + // silently blocking a legitimate revive. Update the kept row's auth + // in place instead, the same primitive `reconnect`/ + // `completeReconnect`/`connectCandidate` already use for this edge. + let integrationId: string | undefined + if (existing && store.onDisconnect === "keep_row") { + const existingForeignKey = resolveForeignKey(existing) + if (existingForeignKey) { + await store.saveAuthByForeignKey(existingForeignKey, auth, tx) + } + integrationId = existing.integrationId ?? undefined + } else { + try { + const inserted = await store.insertRow( + { + workspaceId: input.workspaceId, + auth, + descriptor, + config: extraConfig, + }, + tx, + ) + integrationId = inserted.integrationId + } catch (err) { + if ( + store.duplicateConstraint && + isUniqueViolationError(err, store.duplicateConstraint) + ) { + throw connectionAlreadyConnectedException() + } + throw err + } + } + + if (existing) { + await connectionRepository.update( + { + id: existing.id, + values: { + integrationId: integrationId ?? null, + displayName: descriptor.displayName, + lastError: null, + statusReason: null, + }, + }, + tx, + ) + return await connectionStateService.transition({ + connectionId: existing.id, + event: "connect.completed", + ownerId, + tx, + }) + } + + // Inserted `disconnected` (not `connected`) so the immediately + // following `transition` crosses the inactive->active edge itself — + // that is the only path `ConnectionStateService.transition` consumes + // quota from. Hardcoding `status: "connected"` here would silently + // skip quota consumption for any channel-kind provider connected via + // this method (e.g. a future `token`-strategy channel). + const created = await connectionRepository.insert( + { + workspaceId: input.workspaceId, + provider: input.provider, + kind: provider.kind, + sourceId: descriptor.sourceId, + displayName: descriptor.displayName, + integrationId: integrationId ?? null, + status: "disconnected", + createdBy: input.actorUserId ?? null, + }, + tx, + ) + return await connectionStateService.transition({ + connectionId: created.id, + event: "connect.completed", + ownerId, + tx, + }) + }) + + return await this.subscribeWebhookBestEffort({ + adapter, + auth, + connection, + ownerId, + }) + } + + /** + * `oauth_redirect`/`oauth_popup` connect: creates a `ConnectSession`, then + * builds the provider's `authorizeUrl` with `state = "{sessionId}.{nonce}"` + * — the OAuth callback hub resolves the session from that `state` alone + * (`ConnectSessionService.findByNonce`), before it has any other request + * context. `credential`/`callbackUrl` are resolved by the app-layer caller + * (tenant-aware platform credential + broker/custom-domain callback URL) + * and passed in — this package cannot resolve them itself without + * depending on `apps/builder`. + */ + async startSession(input: { + workspaceId: string + provider: IntegrationType + purpose: ConnectSessionPurpose + credential: ConnectionCredential + callbackUrl: string + targetConnectionId?: string | null + actorUserId?: string | null + actorTokenId?: string | null + platformOwnerId?: string | null + originHost?: string | null + returnUrl?: string | null + }): Promise<{ session: ConnectSessionModel; nextAction: ConnectNextAction }> { + const adapter = this.resolveAdapter(input.provider) + if (!adapter.provider.authorizeUrl) { + throw connectionNotOAuthException(input.provider) + } + + const { session, nonce } = await connectSessionService.create({ + workspaceId: input.workspaceId, + provider: input.provider, + purpose: input.purpose, + targetConnectionId: input.targetConnectionId, + actorUserId: input.actorUserId, + actorTokenId: input.actorTokenId, + platformOwnerId: input.platformOwnerId, + originHost: input.originHost, + returnUrl: input.returnUrl, + }) + + const url = adapter.provider.authorizeUrl({ + credential: input.credential, + callbackUrl: input.callbackUrl, + state: `${session.id}.${nonce}`, + }) + const nextAction: ConnectNextAction = { type: "open_url", url } + const updated = await connectSessionService.submitInput({ + id: session.id, + nextAction, + }) + return { session: updated, nextAction } + } + + /** + * OAuth callback exchange: resolves the session by its `state` nonce, + * exchanges `code` for `auth`, lists connectable candidates, and persists + * them as session targets — `attachAuthorization` always lands on + * `awaiting_selection`; auto-completing a single-target/non-multi-account + * provider is the caller's job (it has the `ConnectionProvider` in scope + * to check `multiAccount` and can immediately follow with + * `connectTargets`). + */ + async completeAuthorization(input: { + sessionId: string + nonce: string + code: string + callbackUrl: string + credential: ConnectionCredential + }): Promise { + const session = await connectSessionService.findByNonce(input.nonce) + if (!session || session.id !== input.sessionId) { + throw connectionStateMismatchException() + } + if (session.status !== "pending") { + throw connectSessionExpiredException( + "This connect session is no longer active.", + ) + } + // No local atomic claim on the session before `exchangeCode` (unlike + // `claimTarget`'s DB-level compare-and-set) — two concurrent calls for + // the same session could both pass this `status === "pending"` check + // and both call `exchangeCode` with the same `code`. Relies on the + // OAuth provider enforcing single-use authorization codes, which every + // provider this connects to does; a genuinely idempotent version would + // need its own compare-and-set (e.g. `pending` -> `authorizing`) here. + + const adapter = this.resolveAdapter(session.provider) + if (!adapter.provider.exchangeCode) { + throw connectionNotOAuthException(session.provider) + } + + let auth: AuthValue + try { + auth = await adapter.provider.exchangeCode({ + code: input.code, + callbackUrl: input.callbackUrl, + credential: input.credential, + }) + } catch (err) { + await connectSessionService.fail({ + id: session.id, + errorCode: "provider_denied", + }) + throw connectionCredentialsRejectedException( + toPublicErrorMessage(err, "The provider rejected the authorization."), + ) + } + + if (session.purpose === "reconnect" && session.targetConnectionId) { + return await this.completeReconnect({ session, auth }) + } + + return await this.listAndAttachCandidates(session, auth) + } + + /** + * Lists connectable candidates for an already-obtained `auth` and + * persists them as the session's `awaiting_selection` targets. Split out + * of `completeAuthorization` so a provider whose credential can be + * satisfied without a fresh OAuth round trip — Messenger's Facebook-SSO + * token reuse (`tryReuseFacebookSsoToken`), which skips the OAuth dialog + * entirely when the user's existing Facebook login already carries every + * required scope — can reach `awaiting_selection` directly from an + * app-layer-constructed `auth`, without a `code`/`nonce` to exchange. + */ + async listAndAttachCandidates( + session: ConnectSessionModel, + auth: AuthValue, + ): Promise { + const adapter = this.resolveAdapter(session.provider) + + let candidates: Awaited< + ReturnType> + > + try { + candidates = adapter.provider.listCandidates + ? await adapter.provider.listCandidates({ auth }) + : [{ ...adapter.provider.describe(auth), auth }] + } catch (err) { + await connectSessionService.fail({ + id: session.id, + errorCode: "provider_error", + }) + throw new Error(toPublicErrorMessage(err, "Failed to list accounts.")) + } + + if (candidates.length === 0) { + await connectSessionService.fail({ + id: session.id, + errorCode: "no_candidates", + }) + throw connectionNoCandidatesException() + } + + const targets = await Promise.all( + candidates.map(async (candidate) => { + if (candidate.alreadyConnected) { + return { + id: candidate.sourceId, + name: candidate.displayName, + avatarUrl: candidate.avatarUrl, + selectable: false, + alreadyConnected: candidate.alreadyConnected, + } + } + const existing = + await connectionRepository.findByProviderAndSourceIdAnyWorkspace({ + provider: session.provider, + sourceId: candidate.sourceId, + }) + if (existing && isActiveConnectionStatus(existing.status)) { + const scope: "this_workspace" | "other_workspace" = + existing.workspaceId === session.workspaceId + ? "this_workspace" + : "other_workspace" + return { + id: candidate.sourceId, + name: candidate.displayName, + avatarUrl: candidate.avatarUrl, + selectable: false, + alreadyConnected: scope, + } + } + return { + id: candidate.sourceId, + name: candidate.displayName, + avatarUrl: candidate.avatarUrl, + selectable: true, + } + }), + ) + + // Encrypts the full candidate list — not just the exchanged `auth` — so + // each candidate's own distinct `auth` (a multi-account provider's + // per-page token, e.g. Messenger) survives to `connectTargets`. For a + // single-target/`describe()`-fallback provider this is a one-element + // array holding the same `auth` `exchangeCode` returned. AAD binds the + // ciphertext to this exact session so it cannot be replayed against + // another session's row. + const encryptedAuth = await encryptUtils.encryptObject( + candidates, + `connect-session:${session.id}`, + ) + return await connectSessionService.attachAuthorization({ + id: session.id, + encryptedAuth, + targets, + }) + } + + /** + * Re-authorizes an existing (typically `needs_reauth`) `Connection` — a + * `startSession` with `purpose: "reconnect"` and `targetConnectionId` set, + * so `completeAuthorization` skips candidate selection entirely and + * verifies the re-granted account's identity matches this exact + * connection instead. + */ + async reconnect(input: { + connectionId: string + workspaceId: string + credential: ConnectionCredential + callbackUrl: string + actorUserId?: string | null + actorTokenId?: string | null + platformOwnerId?: string | null + originHost?: string | null + returnUrl?: string | null + }): Promise<{ session: ConnectSessionModel; nextAction: ConnectNextAction }> { + const connection = await this.findOrThrow({ + connectionId: input.connectionId, + workspaceId: input.workspaceId, + }) + return await this.startSession({ + workspaceId: input.workspaceId, + provider: connection.provider, + purpose: "reconnect", + credential: input.credential, + callbackUrl: input.callbackUrl, + targetConnectionId: connection.id, + actorUserId: input.actorUserId, + actorTokenId: input.actorTokenId, + platformOwnerId: input.platformOwnerId, + originHost: input.originHost, + returnUrl: input.returnUrl, + }) + } + + /** + * `completeAuthorization`'s reconnect path: verifies the freshly + * re-authorized identity (`provider.describe(auth).sourceId`) matches the + * target `Connection`'s own `sourceId` — a user can grant access to a + * DIFFERENT account than the one being reconnected, which must not + * silently overwrite the wrong connection's auth — then saves the new + * auth and transitions the connection back to healthy. + */ + private async completeReconnect(input: { + session: ConnectSessionModel + auth: AuthValue + }): Promise { + const { session, auth } = input + const targetConnectionId = session.targetConnectionId + if (!targetConnectionId) { + throw notFoundException("Connection not found") + } + const connection = await connectionRepository.findById({ + id: targetConnectionId, + }) + if (!connection) { + await connectSessionService.fail({ + id: session.id, + errorCode: "internal_error", + }) + throw notFoundException("Connection not found") + } + + const adapter = this.resolveAdapter(connection.provider) + const descriptor = adapter.provider.describe(auth) + if (descriptor.sourceId !== connection.sourceId) { + await connectSessionService.fail({ + id: session.id, + errorCode: "provider_denied", + }) + throw connectionIdentityMismatchException() + } + + const foreignKey = resolveForeignKey(connection) + if (!(adapter.store && foreignKey)) { + await connectSessionService.fail({ + id: session.id, + errorCode: "internal_error", + }) + throw connectionNotConfiguredException(connection.provider) + } + const store = adapter.store + + const authExpiresAt = + auth.authType === "oauth2" && auth.tokens.expiresAt + ? new Date(auth.tokens.expiresAt) + : null + // `connect.completed`, not `auth.saved`/`recordAuthSaved` — `auth.saved` + // requires the connection to already be ACTIVE (`connected`/`degraded`) + // and throws otherwise (`state.ts`), but reconnect's whole purpose is + // reviving an INACTIVE (`needs_reauth`/`disconnected`) connection. + // `connect.completed` is the FSM event that actually allows that edge + // (and consumes quota on it for a channel-kind connection) — the same + // event `connectFromCredentials`'s revive path uses. + const ownerId = await this.resolveOwnerId(connection) + await db.transaction(async (tx) => { + await store.saveAuthByForeignKey(foreignKey, auth, tx) + await connectionRepository.update( + { id: connection.id, values: { authExpiresAt, lastError: null } }, + tx, + ) + await connectionStateService.transition({ + connectionId: connection.id, + event: "connect.completed", + ownerId, + tx, + }) + }) + + return await connectSessionService.recordResults({ + id: session.id, + results: [ + { + targetId: connection.sourceId, + status: "connected", + connectionId: connection.id, + }, + ], + resultConnectionIds: [connection.id], + }) + } + + /** + * `instagramFacebook`'s satellite table is shared with `instagram` + * (`IntegrationInstagram`, disambiguated by its `type` column — see + * `CONNECTION_STORE_BINDINGS`), but `Inbox.channel` has no matching + * `instagramFacebook` value (`ChannelType` only has `instagram`) — every + * other channel-kind `IntegrationType` literal is already a valid + * `ChannelType`. Only called for `kind === "channel"` providers. + */ + private toChannelType(provider: IntegrationType): ChannelType { + return provider === "instagramFacebook" + ? "instagram" + : (provider as ChannelType) + } + + /** + * Connects one already-authorized `ConnectionCandidate` — the per-target + * unit of work behind `connectTargets`. Channel-kind candidates mint (or + * revive) their `Inbox` row first via `inboxService.create`, since + * `Connection.inboxId` must exist before `store.insertRow` and before + * `connectionStateService.transition`'s own Inbox mirror can run; + * `skipQuota: true` there is required — `transition`'s `applyQuotaEdge` + * is the sole quota consumption point on this path (see `ConnectionService.connectFromCredentials`'s + * revive-or-insert comment above), so also consuming inside + * `inboxService.create` would charge a brand-new channel twice. Otherwise + * mirrors `connectFromCredentials`'s revive-or-insert transaction body. + * + * Scope: a *bare* connect. The per-provider UI conveniences a + * picker-driven connect layers on top (Messenger's persistent-menu + * branding, workspace-logo push, tag-sync enqueue — see + * `apps/builder/src/features/integration-messenger/actions/connect-page.ts`) + * are NOT replicated here; `ConnectionProvider.actions` (reserved) is the + * intended future home for a client to opt into any of those separately. + */ + private async connectCandidate(input: { + workspaceId: string + provider: IntegrationType + candidate: ConnectionCandidate + actorUserId?: string | null + }): Promise { + const adapter = this.resolveAdapter(input.provider) + const { provider } = adapter + if (!adapter.store) { + throw connectionNotConfiguredException(input.provider) + } + const store = adapter.store + const auth = input.candidate.auth as AuthValue + const descriptor = provider.describe(auth) + const extraConfig = provider.candidateToConfig?.(auth) ?? {} + + const existing = await connectionRepository.findByProviderSourceId({ + workspaceId: input.workspaceId, + provider: input.provider, + sourceId: descriptor.sourceId, + }) + if (existing && isActiveConnectionStatus(existing.status)) { + throw connectionAlreadyConnectedException() + } + + const ownerId = await this.resolveOwnerId({ + kind: provider.kind, + workspaceId: input.workspaceId, + }) + + const connection = await db.transaction(async (tx) => { + let inboxId: string | undefined + if (provider.kind === "channel") { + if (!ownerId) { + throw notFoundException("Workspace owner not found") + } + const { inbox } = await inboxService.create({ + data: { + workspaceId: input.workspaceId, + channel: this.toChannelType(input.provider), + sourceId: descriptor.sourceId, + name: descriptor.displayName, + }, + ownerId, + tx, + skipQuota: true, + }) + inboxId = inbox.id + } + + // Revive: for a `keep_row` provider (tiktok/whatsapp/zalo/telegram/ + // webchat) the satellite row was never deleted on disconnect — + // calling `store.insertRow` again would collide with its own unique + // constraint and get miscategorized as `connectionAlreadyConnectedException` + // below, silently blocking a legitimate revive. Update the kept row's + // auth in place instead, the same primitive `reconnect`/ + // `completeReconnect` already use for this edge. A `delete_row` + // provider's satellite row IS gone even when `existing` is set (the + // Connection row outlives its satellite row across a disconnect), so + // it still needs a fresh `insertRow`. + let integrationId: string | undefined + if (existing && store.onDisconnect === "keep_row") { + const existingForeignKey = resolveForeignKey(existing) + if (existingForeignKey) { + await store.saveAuthByForeignKey(existingForeignKey, auth, tx) + } + integrationId = existing.integrationId ?? undefined + } else { + try { + const inserted = await store.insertRow( + { + workspaceId: input.workspaceId, + inboxId, + auth, + descriptor, + config: extraConfig, + }, + tx, + ) + integrationId = inserted.integrationId + } catch (err) { + if ( + store.duplicateConstraint && + isUniqueViolationError(err, store.duplicateConstraint) + ) { + throw connectionAlreadyConnectedException() + } + throw err + } + } + + if (existing) { + await connectionRepository.update( + { + id: existing.id, + values: { + inboxId: inboxId ?? existing.inboxId, + integrationId: integrationId ?? null, + displayName: descriptor.displayName, + lastError: null, + statusReason: null, + }, + }, + tx, + ) + return await connectionStateService.transition({ + connectionId: existing.id, + event: "connect.completed", + ownerId, + tx, + }) + } + + const created = await connectionRepository.insert( + { + workspaceId: input.workspaceId, + provider: input.provider, + kind: provider.kind, + sourceId: descriptor.sourceId, + displayName: descriptor.displayName, + inboxId: inboxId ?? null, + integrationId: integrationId ?? null, + status: "disconnected", + createdBy: input.actorUserId ?? null, + }, + tx, + ) + return await connectionStateService.transition({ + connectionId: created.id, + event: "connect.completed", + ownerId, + tx, + }) + }) + + return await this.subscribeWebhookBestEffort({ + adapter, + auth, + connection, + ownerId, + }) + } + + /** + * Finishes an `awaiting_selection` connect session: atomically claims + * each requested target (safe against a double-submit or two tabs — a + * target claimed by a prior call maps to a `duplicated` outcome, never a + * second connect), then connects it via `connectCandidate`. Never throws + * for a single target's failure — every outcome (`connected`/ + * `duplicated`/`limitReached`/`failed`) is reported back per-target, the + * same vocabulary `CONNECT_ITEM_STATUSES` uses for the picker flows this + * replaces. + */ + async connectTargets(input: { + sessionId: string + workspaceId: string + targetIds: string[] + actorUserId?: string | null + }): Promise<{ + session: ConnectSessionModel + connections: ConnectionModel[] + outcomes: ConnectSessionOutcome[] + }> { + const session = await connectSessionService.findByIdForWorkspace({ + id: input.sessionId, + workspaceId: input.workspaceId, + }) + if (!session) { + throw notFoundException("Connect session not found") + } + if (session.status !== "awaiting_selection" || !session.encryptedAuth) { + throw connectSessionExpiredException( + "This connect session is not awaiting target selection.", + ) + } + + const candidates = await encryptUtils.decryptObject( + session.encryptedAuth, + encryptedCandidatesSchema, + `connect-session:${session.id}`, + ) + const candidateBySourceId = new Map( + candidates.map((candidate) => [candidate.sourceId, candidate]), + ) + + const outcomes: ConnectSessionOutcome[] = [] + const connections: ConnectionModel[] = [] + + for (const targetId of input.targetIds) { + const target = session.targets.find((t) => t.id === targetId) + const candidate = candidateBySourceId.get(targetId) + if (!(target && candidate)) { + outcomes.push({ targetId, status: "failed", reason: "unknown" }) + continue + } + if (!target.selectable) { + outcomes.push( + target.alreadyConnected + ? { targetId, status: "duplicated", reason: "alreadyConnected" } + : { targetId, status: "failed", reason: "notSelectable" }, + ) + continue + } + + const claimed = await connectSessionService.claimTarget({ + id: session.id, + targetId, + }) + if (!claimed) { + outcomes.push({ + targetId, + status: "duplicated", + reason: "alreadyConnected", + }) + continue + } + + try { + const connection = await this.connectCandidate({ + workspaceId: input.workspaceId, + provider: session.provider, + candidate: candidate as ConnectionCandidate, + actorUserId: input.actorUserId, + }) + connections.push(connection) + outcomes.push({ + targetId, + status: "connected", + connectionId: connection.id, + }) + } catch (err) { + if ( + err instanceof ChatbotXException && + err.code === "channelLimitReached" + ) { + outcomes.push({ + targetId, + status: "limitReached", + reason: "workspaceLimit", + }) + } else if ( + err instanceof ChatbotXException && + err.code === "connectionAlreadyConnected" + ) { + outcomes.push({ + targetId, + status: "duplicated", + reason: "alreadyConnected", + }) + } else { + logger.warn( + { err, targetId, provider: session.provider }, + "connectTargets: candidate connect failed", + ) + outcomes.push({ + targetId, + status: "failed", + reason: "providerRejected", + detail: toPublicErrorMessage(err, "Connect failed"), + }) + } + } + } + + const updatedSession = await connectSessionService.recordResults({ + id: session.id, + results: outcomes, + resultConnectionIds: connections.map((connection) => connection.id), + }) + + return { session: updatedSession, connections, outcomes } + } +} + +export const connectionService = new ConnectionService() diff --git a/packages/connections/tsconfig.json b/packages/connections/tsconfig.json new file mode 100644 index 0000000000..281464fc17 --- /dev/null +++ b/packages/connections/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@chatbotx.io/typescript-config/base.json", + "include": ["src/**/*.ts"], + "compilerOptions": { + "strictNullChecks": true + } +} diff --git a/packages/connections/vitest.config.ts b/packages/connections/vitest.config.ts new file mode 100644 index 0000000000..84da752da3 --- /dev/null +++ b/packages/connections/vitest.config.ts @@ -0,0 +1 @@ +export { default } from "@chatbotx.io/vitest-config/node" diff --git a/packages/database/drizzle/20260914163422_consolidate_connection_scope/migration.sql b/packages/database/drizzle/20260914163422_consolidate_connection_scope/migration.sql new file mode 100644 index 0000000000..642efdfb98 --- /dev/null +++ b/packages/database/drizzle/20260914163422_consolidate_connection_scope/migration.sql @@ -0,0 +1,18 @@ +-- Data-only migration: no schema/DDL change. `WorkspaceApiToken.scopes` is a +-- plain `text[]` (see `packages/database/src/partials/workspace-api-token.ts`), +-- not a pg enum, so this only rewrites already-issued tokens' scope values. +-- +-- The `WorkspaceApiTokenScope` enum values "channels" and "integrations" were +-- merged into a single "connections" value — every route that used either +-- scope now requires "connections" instead. This maps both old values to the +-- new one, de-duplicating in case a token already carried both (or already +-- carries "connections" for some other reason). Rows with `scopes IS NULL` +-- (the "All scopes" / unrestricted sentinel) are untouched — the `WHERE` +-- clause only matches rows whose array actually contains one of the old +-- values. +UPDATE "WorkspaceApiToken" +SET "scopes" = ( + SELECT array_agg(DISTINCT CASE WHEN s IN ('channels', 'integrations') THEN 'connections' ELSE s END) + FROM unnest("scopes") AS s +) +WHERE "scopes" && ARRAY['channels', 'integrations']; diff --git a/packages/database/drizzle/20260914163422_consolidate_connection_scope/snapshot.json b/packages/database/drizzle/20260914163422_consolidate_connection_scope/snapshot.json new file mode 100644 index 0000000000..c91eb4c8a8 --- /dev/null +++ b/packages/database/drizzle/20260914163422_consolidate_connection_scope/snapshot.json @@ -0,0 +1,44204 @@ +{ + "version": "8", + "dialect": "postgres", + "id": "38cbc761-a0fe-498b-9731-e73354cc42a8", + "prevIds": [ + "115a0172-9121-4729-acd1-91aa779bea7b" + ], + "ddl": [ + { + "values": [ + "pending", + "sent", + "failed", + "skipped_no_scope", + "skipped_region" + ], + "name": "adsConversionCapiStatus", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "automatic", + "rule", + "trigger" + ], + "name": "adsConversionEventSource", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "whatsapp", + "facebook", + "messenger", + "instagram" + ], + "name": "adsConversionChannel", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "lead", + "purchase" + ], + "name": "adsConversionEventType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "pending", + "success", + "error", + "processing" + ], + "name": "aiConversationEmbeddingStatus", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "pending", + "processing", + "success", + "error" + ], + "name": "aiConversationSourceStatus", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "document", + "image", + "url", + "web_search" + ], + "name": "aiConversationSourceType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "pending", + "success", + "error", + "processing" + ], + "name": "aiEmbeddingStatus", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "automated_response", + "ai_agent", + "flow", + "none" + ], + "name": "analyticsBotResponseType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "success", + "fallback" + ], + "name": "analyticsBotResult", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "flow", + "agent", + "fallback" + ], + "name": "analyticsBotRouteType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "message:sent", + "message:delivered", + "message:seen", + "message:failed", + "flow:clicked" + ], + "name": "analyticsBroadcastEventType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "contact_created", + "contact_deleted", + "contact_blocked" + ], + "name": "analyticsContactEventType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "bot", + "human" + ], + "name": "analyticsContactSenderType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "conversation_created", + "conversation_assigned", + "conversation_unassigned", + "conversation_transferred_to_human", + "conversation_transferred_to_bot", + "conversation_followed", + "conversation_unfollowed", + "conversation_archived", + "conversation_unarchived" + ], + "name": "analyticsConversationEventType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "message_human_sent", + "message_bot_sent" + ], + "name": "analyticsMessageEventType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "processing", + "ingested", + "failed" + ], + "name": "analyticsStatus", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "pending", + "synced", + "failed" + ], + "name": "appointmentExternalSyncStatus", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "inPerson", + "phoneCall", + "onlineMeeting" + ], + "name": "appointmentLocationTypeSnapshot", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "scheduled", + "cancelled" + ], + "name": "appointmentStatus", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "inPerson", + "phoneCall", + "onlineMeeting" + ], + "name": "appointmentLocationType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "rollingDays", + "dateRange", + "specificDay", + "anyFutureDate" + ], + "name": "appointmentScheduleWindowType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "minutes", + "hours", + "days" + ], + "name": "appointmentReminderTimingUnit", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "pending", + "sent", + "cancelled", + "failed" + ], + "name": "appointmentReminderDispatchStatus", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "image", + "video", + "audio", + "gif", + "file" + ], + "name": "fileType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "inbound", + "outbound" + ], + "name": "automatedResponseType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "defaultReply" + ], + "name": "automationThrottleType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "now", + "future" + ], + "name": "broadcastScheduleType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "scheduled", + "sent", + "sending", + "cancelled", + "draft", + "failed" + ], + "name": "broadcastStatus", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "whatsapp", + "messenger", + "instagram" + ], + "name": "coexistChannel", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "contacts", + "messages" + ], + "name": "coexistMessengerSyncPhase", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "init", + "running", + "succeeded", + "failed", + "partial", + "waiting" + ], + "name": "coexistRunStatus", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "coexist", + "contact_scan" + ], + "name": "coexistRunType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "male", + "female", + "unknown" + ], + "name": "gender", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "text", + "location", + "refLink", + "image", + "video", + "audio", + "gif", + "file" + ], + "name": "lastUserInputType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "pending", + "scheduled", + "completed", + "failed", + "canceled" + ], + "name": "ContactOnSmartDelayStatus", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "waitNode", + "followUp" + ], + "name": "ContactOnSmartDelayType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "active", + "archived" + ], + "name": "couponTopicStatus", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "shortText", + "email", + "phoneNumber", + "number", + "date", + "datetime", + "boolean", + "longText" + ], + "name": "customFieldType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "messenger", + "instagram", + "instagramFacebook" + ], + "name": "fbCommentAutomationType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "sent", + "failed" + ], + "name": "commentAutomationEventStatus", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "public", + "private" + ], + "name": "commentAutomationReplyChannel", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "AIAgent", + "text", + "flow", + "none" + ], + "name": "commentAutomationReplyType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "import", + "generic", + "export" + ], + "name": "fileContextType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "pending", + "uploaded", + "failed" + ], + "name": "fileStatus", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "tag", + "flow", + "customField", + "automatedResponse", + "trigger", + "webhook", + "sequence", + "emailTopic", + "fbComment", + "igComment", + "igStory", + "outboundAutomatedResponse" + ], + "name": "folderType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "instagram", + "instagramFacebook" + ], + "name": "igStoryAutomationType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "csv", + "xlsx", + "xls", + "json" + ], + "name": "importFormat", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "pending", + "processing", + "completed", + "failed" + ], + "name": "importStatus", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "contacts", + "coupons", + "products", + "flow" + ], + "name": "importType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "oauth", + "fbe" + ], + "name": "metaCatalogAuthMode", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "active", + "invalid" + ], + "name": "metaCatalogConnectionStatus", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "idle", + "queued", + "running", + "succeeded", + "partial", + "failed" + ], + "name": "metaCatalogImportStatus", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "pending_verification", + "registered", + "failed" + ], + "name": "whatsappRegistrationStatus", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "text", + "location", + "refLink" + ], + "name": "contentType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "message", + "comment" + ], + "name": "messageKind", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "incoming", + "outgoing", + "activity" + ], + "name": "messageType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "bot", + "contact", + "system", + "user", + "api" + ], + "name": "senderType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "pending", + "processing", + "completed", + "failed" + ], + "name": "MessageCleanupStatus", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "whatsapp", + "messenger", + "instagram" + ], + "name": "messagingAdChannel", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "pending", + "campaignCreated", + "adSetCreated", + "creativeCreated", + "adCreated", + "failed" + ], + "name": "messagingAdCreateState", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "draft", + "publishing", + "published", + "pausing", + "paused", + "deleting", + "deleted", + "publishFailed" + ], + "name": "messagingAdPublishState", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "push", + "import" + ], + "name": "metaCatalogItemDirection", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "all", + "category", + "selected" + ], + "name": "metaCatalogSyncScope", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "queued", + "running", + "succeeded", + "partial", + "failed" + ], + "name": "metaCatalogSyncStatus", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "luckyWheel", + "jackpot", + "gashapon", + "drawLots", + "scratchOff" + ], + "name": "minigameType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "dont_track", + "track" + ], + "name": "inventoryPolicy", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "text", + "number", + "email", + "phone", + "multipleChoice", + "date", + "datetime", + "image", + "file", + "location", + "websiteLink" + ], + "name": "questionnaireQuestionType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "inProgress", + "completed", + "cancelled", + "failed", + "timeout" + ], + "name": "questionnaireSubmissionStatus", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "refLink", + "qrCode" + ], + "name": "ReflinkType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "me" + ], + "name": "SystemFieldType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "pending", + "installing", + "completed", + "partial", + "failed" + ], + "name": "templateInstallationStatus", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "flows", + "products", + "aiFunctions", + "aiAgents", + "calendars", + "webchats", + "keywords", + "entryPointLinks", + "triggers", + "fbCommentAutomations", + "settings", + "customFields", + "tags", + "productCategories" + ], + "name": "templateResourceCategory", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "ios", + "android" + ], + "name": "devicePlatform", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "allTime", + "oncePerHour", + "oncePerDay" + ], + "name": "defaultReplyFrequency", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "full", + "read_only" + ], + "name": "WorkspaceApiTokenPermission", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "owner", + "agent" + ], + "name": "workspaceMemberRole", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "outsideSchedule", + "postNotMatched", + "commentIsReply", + "keywordsNotMatched", + "contactNotNew", + "alreadyRepliedOnPost", + "engagedOnOtherPost" + ], + "name": "commentAutomationMissReason", + "entityType": "enums", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "MessageShard", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "ShardTimeRange", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "AdsConversionEvent", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "AdsConversionRule", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "AIAgent", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "AIConversationEmbedding", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "AIConversationSource", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "AIEmbedding", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "AIFile", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "AIFunction", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "AIMCPServer", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "AnalyticsBotMessageEvent", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "AnalyticsBroadcastEvent", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "AnalyticsContactEvent", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "AnalyticsConversationEvent", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "AnalyticsFlowNodeEvent", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "AnalyticsMessageEvent", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "AnalyticsSequenceEvent", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "AnalyticsEmailTopic", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "AnalyticsManifestStatus", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Appointment", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "AppointmentCalendar", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "AppointmentCalendarAvailability", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "AppointmentCalendarReminder", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "AppointmentReminderDispatch", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Attachment", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Account", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Invitation", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Jwk", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Session", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "User", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Verification", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "AutomatedResponse", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "AutomationThrottle", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "BotField", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Broadcast", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "BroadcastTarget", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "CoexistSyncRun", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Contact", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "ContactActiveHourly", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "ContactActiveMonthly", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "ContactCustomField", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "ContactInbox", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "ContactNote", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "ContactOnBroadcast", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "ContactOnSequence", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "ContactOnSmartDelay", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "ContactToTag", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "ContactToTagChannel", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Conversation", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "ConversationParticipant", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Coupon", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "CouponTopic", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "CustomField", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "DynamicImage", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "EmailTopic", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "AuditLog", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "CustomDomain", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Tenant", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "TenantHelpItem", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "UserQuota", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "WorkspaceUsage", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "ErrorLog", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "ExternalWebhook", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "FacebookLeadAdsAutomation", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "FacebookLeadAdsLead", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "FBCommentAutomation", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "FBCommentAutomationEvent", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "FBCommentAutomationReply", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "File", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Flow", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "FlowAnalyticsSession", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "FlowNodeStat", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "FlowRun", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "FlowVersion", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Folder", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IgStoryAutomation", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Import", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Inbox", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "InboxContactStat", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "InboxTeam", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "InboxTeamMember", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationActiveCampaign", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationApi", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Integration", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationClaude", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationDeepseek", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationDrip", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationFacebookAds", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationGemini", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationGetResponse", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationGoogleCalendar", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationGoogleSheet", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationInstagram", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationKlaviyo", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationMailchimp", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationMailerLite", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationMessenger", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationMetaCatalog", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationMoosend", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationOpenai", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationOpenaiCompatible", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationOpenrouter", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationOutlookCalendar", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationSendGrid", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationSmtp", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationTelegram", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationTiktok", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationWebchat", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationWhatsapp", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationZalo", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "MagicLink", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "MagicLinkStat", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "MediaLibraryFile", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "MediaLibraryFolder", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Message", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "MessageCleanup", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "MessagingAdOperation", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "MessagingAdsConnection", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "MessengerMessageTemplate", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "MetaCapiEvent", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "MetaCatalogItem", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "MetaCatalogSyncRun", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Minigame", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "MinigameContact", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "MinigamePlay", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "PlatformCredential", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Product", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "ProductAddon", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "ProductCategory", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "ProductVariant", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "ProductVariantOption", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "QuestionnaireAnswer", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Questionnaire", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "QuestionnaireQuestion", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "QuestionnaireSubmission", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "RefLinkStat", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Reflink", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "SavedReply", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Sequence", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "SequenceDispatch", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "SequenceStep", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Spreadsheet", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "SystemField", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Tag", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "TagChannel", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Template", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "TemplateInstallation", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "TemplateInstalledResource", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Trigger", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Condition", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "TriggerContactHistory", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "TriggerExecution", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "TriggerStat", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "UserDeviceToken", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "UserPersistentMenu", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Webhook", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "WebhookExecution", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "WhatsappBusinessAccount", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "WhatsappCoexistStaging", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "WhatsappFlow", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "WhatsappMessageTemplate", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "WhatsappSignupSession", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Workspace", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "WorkspaceApiToken", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "WorkspaceMac", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "WorkspaceMember", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "FBCommentAutomationMiss", + "entityType": "tables", + "schema": "public" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "MessageShard" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "MessageShard" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "MessageShard" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "MessageShard" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "host", + "entityType": "columns", + "schema": "public", + "table": "MessageShard" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "5432", + "generated": null, + "identity": null, + "name": "port", + "entityType": "columns", + "schema": "public", + "table": "MessageShard" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "database", + "entityType": "columns", + "schema": "public", + "table": "MessageShard" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user", + "entityType": "columns", + "schema": "public", + "table": "MessageShard" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "credentialRef", + "entityType": "columns", + "schema": "public", + "table": "MessageShard" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "'disable'", + "generated": null, + "identity": null, + "name": "sslMode", + "entityType": "columns", + "schema": "public", + "table": "MessageShard" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isActive", + "entityType": "columns", + "schema": "public", + "table": "MessageShard" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isMain", + "entityType": "columns", + "schema": "public", + "table": "MessageShard" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "shardKey", + "entityType": "columns", + "schema": "public", + "table": "MessageShard" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "readHost", + "entityType": "columns", + "schema": "public", + "table": "MessageShard" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "readPort", + "entityType": "columns", + "schema": "public", + "table": "MessageShard" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "ShardTimeRange" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "ShardTimeRange" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "ShardTimeRange" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "shardId", + "entityType": "columns", + "schema": "public", + "table": "ShardTimeRange" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "startTime", + "entityType": "columns", + "schema": "public", + "table": "ShardTimeRange" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "endTime", + "entityType": "columns", + "schema": "public", + "table": "ShardTimeRange" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "type": "adsConversionChannel", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'whatsapp'", + "generated": null, + "identity": null, + "name": "channel", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationWhatsappId", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationMessengerId", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationInstagramId", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "wabaId", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "type": "adsConversionEventSource", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "source", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "type": "adsConversionEventType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "eventType", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ctwaClid", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "adId", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactInboxId", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "currency", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "orderId", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contents", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "occurredAt", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceEventId", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "type": "adsConversionCapiStatus", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "capiStatus", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "capiSentAt", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionRule" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionRule" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionRule" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionRule" + }, + { + "type": "adsConversionChannel", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "channel", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionRule" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationWhatsappId", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionRule" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationFacebookAdsId", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionRule" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationMessengerId", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionRule" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationInstagramId", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionRule" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "adAccountId", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionRule" + }, + { + "type": "adsConversionEventType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "eventType", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionRule" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "trigger", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionRule" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "markAs", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionRule" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "enabled", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionRule" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "AIAgent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "AIAgent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "AIAgent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "AIAgent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "AIAgent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "prompt", + "entityType": "columns", + "schema": "public", + "table": "AIAgent" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "", + "generated": null, + "identity": null, + "name": "messages", + "entityType": "columns", + "schema": "public", + "table": "AIAgent" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isDefault", + "entityType": "columns", + "schema": "public", + "table": "AIAgent" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isRichResponse", + "entityType": "columns", + "schema": "public", + "table": "AIAgent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "", + "generated": null, + "identity": null, + "name": "tools", + "entityType": "columns", + "schema": "public", + "table": "AIAgent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "", + "generated": null, + "identity": null, + "name": "webSearchAuthorizedDomains", + "entityType": "columns", + "schema": "public", + "table": "AIAgent" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "", + "generated": null, + "identity": null, + "name": "models", + "entityType": "columns", + "schema": "public", + "table": "AIAgent" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "temperature", + "entityType": "columns", + "schema": "public", + "table": "AIAgent" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "maxOutputTokens", + "entityType": "columns", + "schema": "public", + "table": "AIAgent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "AIConversationEmbedding" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "AIConversationEmbedding" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "AIConversationEmbedding" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceId", + "entityType": "columns", + "schema": "public", + "table": "AIConversationEmbedding" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "AIConversationEmbedding" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "conversationId", + "entityType": "columns", + "schema": "public", + "table": "AIConversationEmbedding" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "chunkIndex", + "entityType": "columns", + "schema": "public", + "table": "AIConversationEmbedding" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "content", + "entityType": "columns", + "schema": "public", + "table": "AIConversationEmbedding" + }, + { + "type": "vector(1536)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "embedding", + "entityType": "columns", + "schema": "public", + "table": "AIConversationEmbedding" + }, + { + "type": "aiConversationEmbeddingStatus", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "AIConversationEmbedding" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "errorMessage", + "entityType": "columns", + "schema": "public", + "table": "AIConversationEmbedding" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "AIConversationSource" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "AIConversationSource" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "AIConversationSource" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "AIConversationSource" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "conversationId", + "entityType": "columns", + "schema": "public", + "table": "AIConversationSource" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "messageId", + "entityType": "columns", + "schema": "public", + "table": "AIConversationSource" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "attachmentId", + "entityType": "columns", + "schema": "public", + "table": "AIConversationSource" + }, + { + "type": "aiConversationSourceType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceType", + "entityType": "columns", + "schema": "public", + "table": "AIConversationSource" + }, + { + "type": "aiConversationSourceStatus", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "AIConversationSource" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceKey", + "entityType": "columns", + "schema": "public", + "table": "AIConversationSource" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contentHash", + "entityType": "columns", + "schema": "public", + "table": "AIConversationSource" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "mimeType", + "entityType": "columns", + "schema": "public", + "table": "AIConversationSource" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "AIConversationSource" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "metadata", + "entityType": "columns", + "schema": "public", + "table": "AIConversationSource" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "summary", + "entityType": "columns", + "schema": "public", + "table": "AIConversationSource" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "errorMessage", + "entityType": "columns", + "schema": "public", + "table": "AIConversationSource" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "AIEmbedding" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "AIEmbedding" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "AIEmbedding" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "content", + "entityType": "columns", + "schema": "public", + "table": "AIEmbedding" + }, + { + "type": "vector(1536)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "embedding", + "entityType": "columns", + "schema": "public", + "table": "AIEmbedding" + }, + { + "type": "aiEmbeddingStatus", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "AIEmbedding" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "AIEmbedding" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "aiFileId", + "entityType": "columns", + "schema": "public", + "table": "AIEmbedding" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "AIFile" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "AIFile" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "AIFile" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "AIFile" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "path", + "entityType": "columns", + "schema": "public", + "table": "AIFile" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "size", + "entityType": "columns", + "schema": "public", + "table": "AIFile" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "mimeType", + "entityType": "columns", + "schema": "public", + "table": "AIFile" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "AIFile" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "AIFunction" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "AIFunction" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "AIFunction" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "AIFunction" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "purpose", + "entityType": "columns", + "schema": "public", + "table": "AIFunction" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "dataCollect", + "entityType": "columns", + "schema": "public", + "table": "AIFunction" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "outputMessage", + "entityType": "columns", + "schema": "public", + "table": "AIFunction" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "triggerFlowId", + "entityType": "columns", + "schema": "public", + "table": "AIFunction" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "AIFunction" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "AIMCPServer" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "AIMCPServer" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "AIMCPServer" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "AIMCPServer" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "url", + "entityType": "columns", + "schema": "public", + "table": "AIMCPServer" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "AIMCPServer" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "availableTools", + "entityType": "columns", + "schema": "public", + "table": "AIMCPServer" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "", + "generated": null, + "identity": null, + "name": "selectedTools", + "entityType": "columns", + "schema": "public", + "table": "AIMCPServer" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "AIMCPServer" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "eventId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsBotMessageEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsBotMessageEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "messageId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsBotMessageEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "conversationId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsBotMessageEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "occurredAt", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsBotMessageEvent" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "hasResponse", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsBotMessageEvent" + }, + { + "type": "analyticsBotResponseType", + "typeSchema": "public", + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "responseType", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsBotMessageEvent" + }, + { + "type": "analyticsBotRouteType", + "typeSchema": "public", + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "routeType", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsBotMessageEvent" + }, + { + "type": "analyticsBotResult", + "typeSchema": "public", + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "result", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsBotMessageEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "aiProvider", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsBotMessageEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "channel", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsBotMessageEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "source", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsBotMessageEvent" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "metadata", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsBotMessageEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "insertedAt", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsBotMessageEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsBroadcastEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "broadcastId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsBroadcastEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactInboxId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsBroadcastEvent" + }, + { + "type": "analyticsBroadcastEventType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "eventType", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsBroadcastEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "batchId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsBroadcastEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "occurredAt", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsBroadcastEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "insertedAt", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsBroadcastEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "eventId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsContactEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsContactEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsContactEvent" + }, + { + "type": "analyticsContactEventType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "eventType", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsContactEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "occurredAt", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsContactEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "source", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsContactEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsContactEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "channel", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsContactEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "country", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsContactEvent" + }, + { + "type": "analyticsContactSenderType", + "typeSchema": "public", + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "senderType", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsContactEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "adminId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsContactEvent" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "metadata", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsContactEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "insertedAt", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsContactEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "eventId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsConversationEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsConversationEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "conversationId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsConversationEvent" + }, + { + "type": "analyticsConversationEventType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "eventType", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsConversationEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "occurredAt", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsConversationEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fromAssignee", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsConversationEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "toAssignee", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsConversationEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "channel", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsConversationEvent" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "metadata", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsConversationEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "insertedAt", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsConversationEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsFlowNodeEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "flowId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsFlowNodeEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "analyticsId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsFlowNodeEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "nodeId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsFlowNodeEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "buttonId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsFlowNodeEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactInboxId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsFlowNodeEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "eventType", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsFlowNodeEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "occurredAt", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsFlowNodeEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "insertedAt", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsFlowNodeEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "eventId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsMessageEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsMessageEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsMessageEvent" + }, + { + "type": "analyticsMessageEventType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "eventType", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsMessageEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "occurredAt", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsMessageEvent" + }, + { + "type": "analyticsContactSenderType", + "typeSchema": "public", + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "senderType", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsMessageEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "adminId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsMessageEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "channel", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsMessageEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "source", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsMessageEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsMessageEvent" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "metadata", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsMessageEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "insertedAt", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsMessageEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsSequenceEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactInboxId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsSequenceEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "eventType", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsSequenceEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sequenceId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsSequenceEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "stepId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsSequenceEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "occurredAt", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsSequenceEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "insertedAt", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsSequenceEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "topicId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "conversationId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactInboxId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deliveredAt", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "failedAt", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "firstSeenAt", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastSeenAt", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "seenCount", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "firstClickedAt", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastClickedAt", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "clickCount", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "objectKey", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsManifestStatus" + }, + { + "type": "analyticsStatus", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsManifestStatus" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "attempts", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsManifestStatus" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ingestedAt", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsManifestStatus" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastError", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsManifestStatus" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Appointment" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Appointment" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Appointment" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "Appointment" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "calendarId", + "entityType": "columns", + "schema": "public", + "table": "Appointment" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "Appointment" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "conversationId", + "entityType": "columns", + "schema": "public", + "table": "Appointment" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactInboxId", + "entityType": "columns", + "schema": "public", + "table": "Appointment" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "startAt", + "entityType": "columns", + "schema": "public", + "table": "Appointment" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "endAt", + "entityType": "columns", + "schema": "public", + "table": "Appointment" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inviteeTimezone", + "entityType": "columns", + "schema": "public", + "table": "Appointment" + }, + { + "type": "appointmentStatus", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'scheduled'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "Appointment" + }, + { + "type": "appointmentLocationTypeSnapshot", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "locationType", + "entityType": "columns", + "schema": "public", + "table": "Appointment" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "locationDetail", + "entityType": "columns", + "schema": "public", + "table": "Appointment" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "externalEventId", + "entityType": "columns", + "schema": "public", + "table": "Appointment" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "externalEventIntegrationId", + "entityType": "columns", + "schema": "public", + "table": "Appointment" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "externalEventProviderCalendarId", + "entityType": "columns", + "schema": "public", + "table": "Appointment" + }, + { + "type": "appointmentExternalSyncStatus", + "typeSchema": "public", + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "externalSyncStatus", + "entityType": "columns", + "schema": "public", + "table": "Appointment" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "cancelledAt", + "entityType": "columns", + "schema": "public", + "table": "Appointment" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deletedAt", + "entityType": "columns", + "schema": "public", + "table": "Appointment" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "active", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "timezone", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "30", + "generated": null, + "identity": null, + "name": "durationMinutes", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "bufferAfterMinutes", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "appointmentLocationType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "locationType", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "locationDetail", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "appointmentScheduleWindowType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'rollingDays'", + "generated": null, + "identity": null, + "name": "scheduleWindowType", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "scheduleWindowConfig", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "maxAppointmentsPerUser", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "dailyLimitEnabled", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "maxPerDay", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "allowGroupMeeting", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "maxPerSlot", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "confirmationMessage", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "confirmationFlowId", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "cancellationFlowId", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "externalConnectionId", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "externalEventTitleTemplate", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "externalEventDescriptionTemplate", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "externalEventAttendeesTemplate", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publicLinkSlug", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deletedAt", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendarAvailability" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendarAvailability" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendarAvailability" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "calendarId", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendarAvailability" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "weekday", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendarAvailability" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "startMinute", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendarAvailability" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "endMinute", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendarAvailability" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendarReminder" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendarReminder" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendarReminder" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "calendarId", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendarReminder" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "flowId", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendarReminder" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "timingValue", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendarReminder" + }, + { + "type": "appointmentReminderTimingUnit", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "timingUnit", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendarReminder" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "AppointmentReminderDispatch" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "AppointmentReminderDispatch" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "AppointmentReminderDispatch" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "AppointmentReminderDispatch" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "appointmentId", + "entityType": "columns", + "schema": "public", + "table": "AppointmentReminderDispatch" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reminderConfigId", + "entityType": "columns", + "schema": "public", + "table": "AppointmentReminderDispatch" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactInboxId", + "entityType": "columns", + "schema": "public", + "table": "AppointmentReminderDispatch" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sendAt", + "entityType": "columns", + "schema": "public", + "table": "AppointmentReminderDispatch" + }, + { + "type": "appointmentReminderDispatchStatus", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "AppointmentReminderDispatch" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "jobId", + "entityType": "columns", + "schema": "public", + "table": "AppointmentReminderDispatch" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sentAt", + "entityType": "columns", + "schema": "public", + "table": "AppointmentReminderDispatch" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "cancelledAt", + "entityType": "columns", + "schema": "public", + "table": "AppointmentReminderDispatch" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "failedReason", + "entityType": "columns", + "schema": "public", + "table": "AppointmentReminderDispatch" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Attachment" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Attachment" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Attachment" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "Attachment" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "conversationId", + "entityType": "columns", + "schema": "public", + "table": "Attachment" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "messageId", + "entityType": "columns", + "schema": "public", + "table": "Attachment" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "messageCreatedAt", + "entityType": "columns", + "schema": "public", + "table": "Attachment" + }, + { + "type": "fileType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fileType", + "entityType": "columns", + "schema": "public", + "table": "Attachment" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceId", + "entityType": "columns", + "schema": "public", + "table": "Attachment" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "mimeType", + "entityType": "columns", + "schema": "public", + "table": "Attachment" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "width", + "entityType": "columns", + "schema": "public", + "table": "Attachment" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "height", + "entityType": "columns", + "schema": "public", + "table": "Attachment" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "size", + "entityType": "columns", + "schema": "public", + "table": "Attachment" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "thumbnailPath", + "entityType": "columns", + "schema": "public", + "table": "Attachment" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "originPath", + "entityType": "columns", + "schema": "public", + "table": "Attachment" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "Attachment" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Account" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Account" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "accountId", + "entityType": "columns", + "schema": "public", + "table": "Account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerId", + "entityType": "columns", + "schema": "public", + "table": "Account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "accessToken", + "entityType": "columns", + "schema": "public", + "table": "Account" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "accessTokenExpiresAt", + "entityType": "columns", + "schema": "public", + "table": "Account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refreshToken", + "entityType": "columns", + "schema": "public", + "table": "Account" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refreshTokenExpiresAt", + "entityType": "columns", + "schema": "public", + "table": "Account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "scope", + "entityType": "columns", + "schema": "public", + "table": "Account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "idToken", + "entityType": "columns", + "schema": "public", + "table": "Account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "password", + "entityType": "columns", + "schema": "public", + "table": "Account" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "Account" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "tenantId", + "entityType": "columns", + "schema": "public", + "table": "Account" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Invitation" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Invitation" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "code", + "entityType": "columns", + "schema": "public", + "table": "Invitation" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "permissions", + "entityType": "columns", + "schema": "public", + "table": "Invitation" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "Invitation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "Invitation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "invitedBy", + "entityType": "columns", + "schema": "public", + "table": "Invitation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Jwk" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Jwk" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Jwk" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publicKey", + "entityType": "columns", + "schema": "public", + "table": "Jwk" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "privateKey", + "entityType": "columns", + "schema": "public", + "table": "Jwk" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "Jwk" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Session" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Session" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Session" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "Session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "public", + "table": "Session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ipAddress", + "entityType": "columns", + "schema": "public", + "table": "Session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userAgent", + "entityType": "columns", + "schema": "public", + "table": "Session" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "Session" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "User" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "User" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "User" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "User" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "User" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "emailVerified", + "entityType": "columns", + "schema": "public", + "table": "User" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "image", + "entityType": "columns", + "schema": "public", + "table": "User" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isAnonymous", + "entityType": "columns", + "schema": "public", + "table": "User" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "mustChangePassword", + "entityType": "columns", + "schema": "public", + "table": "User" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "tenantId", + "entityType": "columns", + "schema": "public", + "table": "User" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Verification" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Verification" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Verification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "identifier", + "entityType": "columns", + "schema": "public", + "table": "Verification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "public", + "table": "Verification" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "Verification" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "AutomatedResponse" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "AutomatedResponse" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "AutomatedResponse" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "AutomatedResponse" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "folderId", + "entityType": "columns", + "schema": "public", + "table": "AutomatedResponse" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "", + "generated": null, + "identity": null, + "name": "keywords", + "entityType": "columns", + "schema": "public", + "table": "AutomatedResponse" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "AutomatedResponse" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "text", + "entityType": "columns", + "schema": "public", + "table": "AutomatedResponse" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "flowId", + "entityType": "columns", + "schema": "public", + "table": "AutomatedResponse" + }, + { + "type": "automatedResponseType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'inbound'", + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "AutomatedResponse" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "AutomationThrottle" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactInboxId", + "entityType": "columns", + "schema": "public", + "table": "AutomationThrottle" + }, + { + "type": "automationThrottleType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "throttleType", + "entityType": "columns", + "schema": "public", + "table": "AutomationThrottle" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "subjectId", + "entityType": "columns", + "schema": "public", + "table": "AutomationThrottle" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "lastTriggeredAt", + "entityType": "columns", + "schema": "public", + "table": "AutomationThrottle" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "claimId", + "entityType": "columns", + "schema": "public", + "table": "AutomationThrottle" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "BotField" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "BotField" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "BotField" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "BotField" + }, + { + "type": "customFieldType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "BotField" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "public", + "table": "BotField" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "BotField" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "folderId", + "entityType": "columns", + "schema": "public", + "table": "BotField" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "BotField" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Broadcast" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Broadcast" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Broadcast" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "Broadcast" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "Broadcast" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "flowId", + "entityType": "columns", + "schema": "public", + "table": "Broadcast" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationWhatsappId", + "entityType": "columns", + "schema": "public", + "table": "Broadcast" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationMessengerId", + "entityType": "columns", + "schema": "public", + "table": "Broadcast" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "templateId", + "entityType": "columns", + "schema": "public", + "table": "Broadcast" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "templateData", + "entityType": "columns", + "schema": "public", + "table": "Broadcast" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'channel'", + "generated": null, + "identity": null, + "name": "targetMode", + "entityType": "columns", + "schema": "public", + "table": "Broadcast" + }, + { + "type": "broadcastStatus", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "Broadcast" + }, + { + "type": "broadcastScheduleType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "schedulesType", + "entityType": "columns", + "schema": "public", + "table": "Broadcast" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "schedulesAt", + "entityType": "columns", + "schema": "public", + "table": "Broadcast" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactFilter", + "entityType": "columns", + "schema": "public", + "table": "Broadcast" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "subaction", + "entityType": "columns", + "schema": "public", + "table": "Broadcast" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "channel", + "entityType": "columns", + "schema": "public", + "table": "Broadcast" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactCount", + "entityType": "columns", + "schema": "public", + "table": "Broadcast" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "handoffCompletedAt", + "entityType": "columns", + "schema": "public", + "table": "Broadcast" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deletedAt", + "entityType": "columns", + "schema": "public", + "table": "Broadcast" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "resumeCount", + "entityType": "columns", + "schema": "public", + "table": "Broadcast" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "broadcastId", + "entityType": "columns", + "schema": "public", + "table": "BroadcastTarget" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inboxId", + "entityType": "columns", + "schema": "public", + "table": "BroadcastTarget" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "flowId", + "entityType": "columns", + "schema": "public", + "table": "BroadcastTarget" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "templateId", + "entityType": "columns", + "schema": "public", + "table": "BroadcastTarget" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "templateData", + "entityType": "columns", + "schema": "public", + "table": "BroadcastTarget" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationId", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "coexistChannel", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "channel", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "coexistRunType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'coexist'", + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "coexistRunStatus", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'init'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "triggerSource", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "startedAt", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "finishedAt", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastHeartbeatAt", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "totalScan", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "currentScan", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "currentStep", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastSyncedAt", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "currentPageNumber", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "importedContactCount", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "importedMessageCount", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "skippedCount", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "failedCount", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "attempts", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "currentError", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastPhase", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastChunkOrder", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "syncProgress", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "coexistMessengerSyncPhase", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'contacts'", + "generated": null, + "identity": null, + "name": "messengerSyncPhase", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pendingPatches", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "claimToken", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "scanFromAt", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "requestedByUserId", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "resumeCursor", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Contact" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Contact" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Contact" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "avatar", + "entityType": "columns", + "schema": "public", + "table": "Contact" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "phoneNumber", + "entityType": "columns", + "schema": "public", + "table": "Contact" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "Contact" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "emailVerified", + "entityType": "columns", + "schema": "public", + "table": "Contact" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "emailOptIn", + "entityType": "columns", + "schema": "public", + "table": "Contact" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "firstName", + "entityType": "columns", + "schema": "public", + "table": "Contact" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastName", + "entityType": "columns", + "schema": "public", + "table": "Contact" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": { + "as": "CASE\n WHEN \"firstName\" IS NULL AND \"lastName\" IS NULL THEN NULL\n WHEN \"firstName\" IS NULL THEN \"lastName\"\n WHEN \"lastName\" IS NULL THEN \"firstName\"\n ELSE \"firstName\" || ' ' || \"lastName\"\n END", + "type": "stored" + }, + "identity": null, + "name": "fullName", + "entityType": "columns", + "schema": "public", + "table": "Contact" + }, + { + "type": "gender", + "typeSchema": "public", + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "gender", + "entityType": "columns", + "schema": "public", + "table": "Contact" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastReadAt", + "entityType": "columns", + "schema": "public", + "table": "Contact" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ref", + "entityType": "columns", + "schema": "public", + "table": "Contact" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "country", + "entityType": "columns", + "schema": "public", + "table": "Contact" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "state", + "entityType": "columns", + "schema": "public", + "table": "Contact" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "city", + "entityType": "columns", + "schema": "public", + "table": "Contact" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "location", + "entityType": "columns", + "schema": "public", + "table": "Contact" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "locale", + "entityType": "columns", + "schema": "public", + "table": "Contact" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "timezone", + "entityType": "columns", + "schema": "public", + "table": "Contact" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "subscribedAt", + "entityType": "columns", + "schema": "public", + "table": "Contact" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "broadcastSubscribedAt", + "entityType": "columns", + "schema": "public", + "table": "Contact" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "blockedAt", + "entityType": "columns", + "schema": "public", + "table": "Contact" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "Contact" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "ContactActiveHourly" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "ContactActiveHourly" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactInboxId", + "entityType": "columns", + "schema": "public", + "table": "ContactActiveHourly" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hourBucket", + "entityType": "columns", + "schema": "public", + "table": "ContactActiveHourly" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inboxId", + "entityType": "columns", + "schema": "public", + "table": "ContactActiveHourly" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "ContactActiveMonthly" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "ContactActiveMonthly" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactInboxId", + "entityType": "columns", + "schema": "public", + "table": "ContactActiveMonthly" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "periodStart", + "entityType": "columns", + "schema": "public", + "table": "ContactActiveMonthly" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inboxId", + "entityType": "columns", + "schema": "public", + "table": "ContactActiveMonthly" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceMacId", + "entityType": "columns", + "schema": "public", + "table": "ContactActiveMonthly" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "ContactCustomField" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "ContactCustomField" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "ContactCustomField" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "public", + "table": "ContactCustomField" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "ContactCustomField" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "customFieldId", + "entityType": "columns", + "schema": "public", + "table": "ContactCustomField" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "originalContactId", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inboxId", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "channel", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "source", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceId", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "language", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "personaId", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactLastReadAt", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "firstInteractionAt", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastMessageAt", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastIncomingMessageAt", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastOutboundMessageAt", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "referral", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastCommentMessageId", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastCommentMessageAt", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "consecutiveFailedReply", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastInputFailure", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastErrorLog", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastBtnTitle", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastUserInput", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "lastUserInputType", + "typeSchema": "public", + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastUserInputType", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "webchatParentUrl", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceUserId", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceUsername", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "ContactNote" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "ContactNote" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "ContactNote" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "text", + "entityType": "columns", + "schema": "public", + "table": "ContactNote" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "ContactNote" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "createdById", + "entityType": "columns", + "schema": "public", + "table": "ContactNote" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "broadcastId", + "entityType": "columns", + "schema": "public", + "table": "ContactOnBroadcast" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "ContactOnBroadcast" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactInboxId", + "entityType": "columns", + "schema": "public", + "table": "ContactOnBroadcast" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "conversationId", + "entityType": "columns", + "schema": "public", + "table": "ContactOnBroadcast" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "sent", + "entityType": "columns", + "schema": "public", + "table": "ContactOnBroadcast" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "seenAt", + "entityType": "columns", + "schema": "public", + "table": "ContactOnBroadcast" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deliveredAt", + "entityType": "columns", + "schema": "public", + "table": "ContactOnBroadcast" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "clickedAt", + "entityType": "columns", + "schema": "public", + "table": "ContactOnBroadcast" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "failedAt", + "entityType": "columns", + "schema": "public", + "table": "ContactOnBroadcast" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "errorContent", + "entityType": "columns", + "schema": "public", + "table": "ContactOnBroadcast" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": { + "as": "case when \"seenAt\" is null then false when \"deliveredAt\" is null then false else \"seenAt\" >= \"deliveredAt\" end", + "type": "stored" + }, + "identity": null, + "name": "isRead", + "entityType": "columns", + "schema": "public", + "table": "ContactOnBroadcast" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "enrolledAt", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "completedAt", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "currentStep", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "nextRunAt", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastStepId", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "nextStepId", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lockedAt", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lockOwner", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastError", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sequenceId", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSmartDelay" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSmartDelay" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "flowId", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSmartDelay" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "flowVersionId", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSmartDelay" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactInboxId", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSmartDelay" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "appointmentId", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSmartDelay" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "conversationId", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSmartDelay" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "nodeId", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSmartDelay" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "stepId", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSmartDelay" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "metadata", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSmartDelay" + }, + { + "type": "ContactOnSmartDelayType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSmartDelay" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSmartDelay" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "triggerAt", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSmartDelay" + }, + { + "type": "ContactOnSmartDelayStatus", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSmartDelay" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "ContactToTag" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tagId", + "entityType": "columns", + "schema": "public", + "table": "ContactToTag" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tagId", + "entityType": "columns", + "schema": "public", + "table": "ContactToTagChannel" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tagChannelId", + "entityType": "columns", + "schema": "public", + "table": "ContactToTagChannel" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactInboxId", + "entityType": "columns", + "schema": "public", + "table": "ContactToTagChannel" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "ContactToTagChannel" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Conversation" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Conversation" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Conversation" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "botEnabled", + "entityType": "columns", + "schema": "public", + "table": "Conversation" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "botResumeAt", + "entityType": "columns", + "schema": "public", + "table": "Conversation" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "archivedAt", + "entityType": "columns", + "schema": "public", + "table": "Conversation" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "additionalAttributes", + "entityType": "columns", + "schema": "public", + "table": "Conversation" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactLastReadAt", + "entityType": "columns", + "schema": "public", + "table": "Conversation" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "agentLastReadAt", + "entityType": "columns", + "schema": "public", + "table": "Conversation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "aiContextLastMessageId", + "entityType": "columns", + "schema": "public", + "table": "Conversation" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastActivityAt", + "entityType": "columns", + "schema": "public", + "table": "Conversation" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "followed", + "entityType": "columns", + "schema": "public", + "table": "Conversation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "assignedUserId", + "entityType": "columns", + "schema": "public", + "table": "Conversation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "assignedInboxTeamId", + "entityType": "columns", + "schema": "public", + "table": "Conversation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "Conversation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "Conversation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceId", + "entityType": "columns", + "schema": "public", + "table": "Conversation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastStep", + "entityType": "columns", + "schema": "public", + "table": "Conversation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "currentStep", + "entityType": "columns", + "schema": "public", + "table": "Conversation" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "adminRepliedAt", + "entityType": "columns", + "schema": "public", + "table": "Conversation" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactRepliedAt", + "entityType": "columns", + "schema": "public", + "table": "Conversation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "ConversationParticipant" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "ConversationParticipant" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "ConversationParticipant" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "ConversationParticipant" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "conversationId", + "entityType": "columns", + "schema": "public", + "table": "ConversationParticipant" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "ConversationParticipant" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Coupon" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Coupon" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Coupon" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "Coupon" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "topicId", + "entityType": "columns", + "schema": "public", + "table": "Coupon" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "code", + "entityType": "columns", + "schema": "public", + "table": "Coupon" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "issuedContactId", + "entityType": "columns", + "schema": "public", + "table": "Coupon" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "issuedAt", + "entityType": "columns", + "schema": "public", + "table": "Coupon" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "usedAt", + "entityType": "columns", + "schema": "public", + "table": "Coupon" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "CouponTopic" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "CouponTopic" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "CouponTopic" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "CouponTopic" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "CouponTopic" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "CouponTopic" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "CouponTopic" + }, + { + "type": "couponTopicStatus", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'active'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "CouponTopic" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deletedAt", + "entityType": "columns", + "schema": "public", + "table": "CouponTopic" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "hasEverHadCoupon", + "entityType": "columns", + "schema": "public", + "table": "CouponTopic" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "createdById", + "entityType": "columns", + "schema": "public", + "table": "CouponTopic" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "CustomField" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "CustomField" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "CustomField" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "CustomField" + }, + { + "type": "customFieldType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "CustomField" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "CustomField" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "folderId", + "entityType": "columns", + "schema": "public", + "table": "CustomField" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "showInInbox", + "entityType": "columns", + "schema": "public", + "table": "CustomField" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "CustomField" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "DynamicImage" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "DynamicImage" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "DynamicImage" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "DynamicImage" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "DynamicImage" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "customFieldId", + "entityType": "columns", + "schema": "public", + "table": "DynamicImage" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "data", + "entityType": "columns", + "schema": "public", + "table": "DynamicImage" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "backgroundUrl", + "entityType": "columns", + "schema": "public", + "table": "DynamicImage" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "enabled", + "entityType": "columns", + "schema": "public", + "table": "DynamicImage" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "EmailTopic" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "EmailTopic" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "EmailTopic" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "EmailTopic" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "EmailTopic" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "folderId", + "entityType": "columns", + "schema": "public", + "table": "EmailTopic" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "sendsTotal", + "entityType": "columns", + "schema": "public", + "table": "EmailTopic" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "deliveredsTotal", + "entityType": "columns", + "schema": "public", + "table": "EmailTopic" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "seensTotal", + "entityType": "columns", + "schema": "public", + "table": "EmailTopic" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "clicksTotal", + "entityType": "columns", + "schema": "public", + "table": "EmailTopic" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "AuditLog" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "AuditLog" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "AuditLog" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "action", + "entityType": "columns", + "schema": "public", + "table": "AuditLog" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "detail", + "entityType": "columns", + "schema": "public", + "table": "AuditLog" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ipAddress", + "entityType": "columns", + "schema": "public", + "table": "AuditLog" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userAgent", + "entityType": "columns", + "schema": "public", + "table": "AuditLog" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "source", + "entityType": "columns", + "schema": "public", + "table": "AuditLog" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "AuditLog" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "AuditLog" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "CustomDomain" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "CustomDomain" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "CustomDomain" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tenantId", + "entityType": "columns", + "schema": "public", + "table": "CustomDomain" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "domain", + "entityType": "columns", + "schema": "public", + "table": "CustomDomain" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "CustomDomain" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "verifiedAt", + "entityType": "columns", + "schema": "public", + "table": "CustomDomain" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "cfHostnameId", + "entityType": "columns", + "schema": "public", + "table": "CustomDomain" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "cfOwnershipValue", + "entityType": "columns", + "schema": "public", + "table": "CustomDomain" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "cfAcmeValue", + "entityType": "columns", + "schema": "public", + "table": "CustomDomain" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Tenant" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Tenant" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Tenant" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ownerId", + "entityType": "columns", + "schema": "public", + "table": "Tenant" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'active'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "Tenant" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "disabledReason", + "entityType": "columns", + "schema": "public", + "table": "Tenant" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "brandName", + "entityType": "columns", + "schema": "public", + "table": "Tenant" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "logoLightPath", + "entityType": "columns", + "schema": "public", + "table": "Tenant" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "logoDarkPath", + "entityType": "columns", + "schema": "public", + "table": "Tenant" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "faviconPath", + "entityType": "columns", + "schema": "public", + "table": "Tenant" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "customCss", + "entityType": "columns", + "schema": "public", + "table": "Tenant" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "customJs", + "entityType": "columns", + "schema": "public", + "table": "Tenant" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "theme", + "entityType": "columns", + "schema": "public", + "table": "Tenant" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "storageUrl", + "entityType": "columns", + "schema": "public", + "table": "Tenant" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "policyUrl", + "entityType": "columns", + "schema": "public", + "table": "Tenant" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "termsOfServiceUrl", + "entityType": "columns", + "schema": "public", + "table": "Tenant" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "signupEmailTemplate", + "entityType": "columns", + "schema": "public", + "table": "Tenant" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "forgotPasswordEmailTemplate", + "entityType": "columns", + "schema": "public", + "table": "Tenant" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "magicLinkEmailTemplate", + "entityType": "columns", + "schema": "public", + "table": "Tenant" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "accountCredentialsEmailTemplate", + "entityType": "columns", + "schema": "public", + "table": "Tenant" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenChannels", + "entityType": "columns", + "schema": "public", + "table": "Tenant" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "TenantHelpItem" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "TenantHelpItem" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "TenantHelpItem" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tenantId", + "entityType": "columns", + "schema": "public", + "table": "TenantHelpItem" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "TenantHelpItem" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "url", + "entityType": "columns", + "schema": "public", + "table": "TenantHelpItem" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "icon", + "entityType": "columns", + "schema": "public", + "table": "TenantHelpItem" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "TenantHelpItem" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactsLimit", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "contactsUsed", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspacesLimit", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "workspacesUsed", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "channelsLimit", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "channelsUsed", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "teamMembersLimit", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "teamMembersUsed", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "macLimit", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "macUsed", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "botMessagesLimit", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "botMessagesUsed", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "monthlyBotMessagesLimit", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "monthlyBotMessagesUsed", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "monthlyBotMessagesPeriodStart", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "botMessagesTopUpGranted", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "whiteLabel", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "ssoSaml", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "saasMode", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "planName", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "planStatus", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "selectedTrialPlanId", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "periodStart", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "periodEnd", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "channelsTornDownAt", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "syncedAt", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceUsage" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceUsage" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceUsage" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceUsage" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "contactsUsed", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceUsage" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "channelsUsed", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceUsage" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "teamMembersUsed", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceUsage" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "botMessagesUsed", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceUsage" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "macUsed", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceUsage" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "syncedAt", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceUsage" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "ErrorLog" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "ErrorLog" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "ErrorLog" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "action", + "entityType": "columns", + "schema": "public", + "table": "ErrorLog" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "detail", + "entityType": "columns", + "schema": "public", + "table": "ErrorLog" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "httpCode", + "entityType": "columns", + "schema": "public", + "table": "ErrorLog" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "ErrorLog" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "ErrorLog" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceId", + "entityType": "columns", + "schema": "public", + "table": "ErrorLog" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "stackTrace", + "entityType": "columns", + "schema": "public", + "table": "ErrorLog" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "ExternalWebhook" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "ExternalWebhook" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "ExternalWebhook" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'make'", + "generated": null, + "identity": null, + "name": "provider", + "entityType": "columns", + "schema": "public", + "table": "ExternalWebhook" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "event", + "entityType": "columns", + "schema": "public", + "table": "ExternalWebhook" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "url", + "entityType": "columns", + "schema": "public", + "table": "ExternalWebhook" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "ExternalWebhook" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "FacebookLeadAdsAutomation" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "FacebookLeadAdsAutomation" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "FacebookLeadAdsAutomation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "FacebookLeadAdsAutomation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "FacebookLeadAdsAutomation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pageId", + "entityType": "columns", + "schema": "public", + "table": "FacebookLeadAdsAutomation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pageName", + "entityType": "columns", + "schema": "public", + "table": "FacebookLeadAdsAutomation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "formId", + "entityType": "columns", + "schema": "public", + "table": "FacebookLeadAdsAutomation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "formName", + "entityType": "columns", + "schema": "public", + "table": "FacebookLeadAdsAutomation" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "fieldMapping", + "entityType": "columns", + "schema": "public", + "table": "FacebookLeadAdsAutomation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "flowId", + "entityType": "columns", + "schema": "public", + "table": "FacebookLeadAdsAutomation" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "leadsHandledCount", + "entityType": "columns", + "schema": "public", + "table": "FacebookLeadAdsAutomation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "FacebookLeadAdsLead" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "FacebookLeadAdsLead" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "FacebookLeadAdsLead" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "automationId", + "entityType": "columns", + "schema": "public", + "table": "FacebookLeadAdsLead" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "leadgenId", + "entityType": "columns", + "schema": "public", + "table": "FacebookLeadAdsLead" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "FacebookLeadAdsLead" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "folderId", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "type": "fbCommentAutomationType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'messenger'", + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "isActive", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "startTime", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "endTime", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "repliesCount", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{\"type\":\"all\",\"value\":[]}'", + "generated": null, + "identity": null, + "name": "post", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{\"type\":\"text\",\"value\":\"\"}'", + "generated": null, + "identity": null, + "name": "privateReply", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{\"type\":\"none\",\"value\":null}'", + "generated": null, + "identity": null, + "name": "publicReply", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{\"type\":\"all\",\"value\":[]}'", + "generated": null, + "identity": null, + "name": "includeKeywords", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "ARRAY[]", + "generated": null, + "identity": null, + "name": "excludeKeywords", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{\"replyToNewContactsOnly\":false,\"replyOncePerUserPerPost\":false,\"likeUserComment\":false,\"replyToUsersWhoCommentedOnOtherPosts\":true,\"ignoreCommentReplies\":true,\"trackUserTags\":false}'", + "generated": null, + "identity": null, + "name": "options", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{\"all\":false,\"hasPhoneNumber\":false,\"hasImage\":false,\"hasVideo\":false,\"hasLink\":false,\"hasKeywords\":false,\"keywords\":[],\"showCommentsAfter\":\"none\"}'", + "generated": null, + "identity": null, + "name": "hideComments", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{\"type\":\"immediately\",\"value\":0}'", + "generated": null, + "identity": null, + "name": "replyAfter", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "automationId", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "postId", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "commentId", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "commentText", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "type": "commentAutomationReplyChannel", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "replyChannel", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "type": "commentAutomationReplyType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "replyType", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "replyText", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "type": "commentAutomationEventStatus", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "errorDetail", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "httpCode", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "occurredAt", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationReply" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationReply" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationReply" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "automationId", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationReply" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationReply" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "postId", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationReply" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationReply" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "File" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "File" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "File" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "File" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "File" + }, + { + "type": "fileContextType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contextType", + "entityType": "columns", + "schema": "public", + "table": "File" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "subType", + "entityType": "columns", + "schema": "public", + "table": "File" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "path", + "entityType": "columns", + "schema": "public", + "table": "File" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fileName", + "entityType": "columns", + "schema": "public", + "table": "File" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "mimeType", + "entityType": "columns", + "schema": "public", + "table": "File" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fileSize", + "entityType": "columns", + "schema": "public", + "table": "File" + }, + { + "type": "fileStatus", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "File" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "meta", + "entityType": "columns", + "schema": "public", + "table": "File" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "uploadedAt", + "entityType": "columns", + "schema": "public", + "table": "File" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Flow" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Flow" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Flow" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "Flow" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "active", + "entityType": "columns", + "schema": "public", + "table": "Flow" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "enableInInbox", + "entityType": "columns", + "schema": "public", + "table": "Flow" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "currentVersionId", + "entityType": "columns", + "schema": "public", + "table": "Flow" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "draftVersionId", + "entityType": "columns", + "schema": "public", + "table": "Flow" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "Flow" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "folderId", + "entityType": "columns", + "schema": "public", + "table": "Flow" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "FlowAnalyticsSession" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "FlowAnalyticsSession" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "FlowAnalyticsSession" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "flowId", + "entityType": "columns", + "schema": "public", + "table": "FlowAnalyticsSession" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "FlowAnalyticsSession" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deletedAt", + "entityType": "columns", + "schema": "public", + "table": "FlowAnalyticsSession" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "FlowNodeStat" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "FlowNodeStat" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "FlowNodeStat" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "FlowNodeStat" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "flowId", + "entityType": "columns", + "schema": "public", + "table": "FlowNodeStat" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "analyticsId", + "entityType": "columns", + "schema": "public", + "table": "FlowNodeStat" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "nodeId", + "entityType": "columns", + "schema": "public", + "table": "FlowNodeStat" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "buttonId", + "entityType": "columns", + "schema": "public", + "table": "FlowNodeStat" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "FlowNodeStat" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactInboxId", + "entityType": "columns", + "schema": "public", + "table": "FlowNodeStat" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "eventType", + "entityType": "columns", + "schema": "public", + "table": "FlowNodeStat" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "errorContent", + "entityType": "columns", + "schema": "public", + "table": "FlowNodeStat" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "occurredAt", + "entityType": "columns", + "schema": "public", + "table": "FlowNodeStat" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "seenAt", + "entityType": "columns", + "schema": "public", + "table": "FlowNodeStat" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refId", + "entityType": "columns", + "schema": "public", + "table": "FlowNodeStat" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refType", + "entityType": "columns", + "schema": "public", + "table": "FlowNodeStat" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "FlowRun" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "FlowRun" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "FlowRun" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "FlowRun" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "flowId", + "entityType": "columns", + "schema": "public", + "table": "FlowRun" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "flowVersionId", + "entityType": "columns", + "schema": "public", + "table": "FlowRun" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "conversationId", + "entityType": "columns", + "schema": "public", + "table": "FlowRun" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "FlowVersion" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "FlowVersion" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "FlowVersion" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "FlowVersion" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "flowId", + "entityType": "columns", + "schema": "public", + "table": "FlowVersion" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": null, + "generated": null, + "identity": null, + "name": "nodes", + "entityType": "columns", + "schema": "public", + "table": "FlowVersion" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": null, + "generated": null, + "identity": null, + "name": "edges", + "entityType": "columns", + "schema": "public", + "table": "FlowVersion" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "isDraft", + "entityType": "columns", + "schema": "public", + "table": "FlowVersion" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isLatest", + "entityType": "columns", + "schema": "public", + "table": "FlowVersion" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "startNodeId", + "entityType": "columns", + "schema": "public", + "table": "FlowVersion" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Folder" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Folder" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Folder" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "Folder" + }, + { + "type": "folderType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "folderType", + "entityType": "columns", + "schema": "public", + "table": "Folder" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "parentId", + "entityType": "columns", + "schema": "public", + "table": "Folder" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "Folder" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isTrash", + "entityType": "columns", + "schema": "public", + "table": "Folder" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "", + "generated": null, + "identity": null, + "name": "paths", + "entityType": "columns", + "schema": "public", + "table": "Folder" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IgStoryAutomation" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IgStoryAutomation" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IgStoryAutomation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "IgStoryAutomation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IgStoryAutomation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "folderId", + "entityType": "columns", + "schema": "public", + "table": "IgStoryAutomation" + }, + { + "type": "igStoryAutomationType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "IgStoryAutomation" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "isActive", + "entityType": "columns", + "schema": "public", + "table": "IgStoryAutomation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "startTime", + "entityType": "columns", + "schema": "public", + "table": "IgStoryAutomation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "endTime", + "entityType": "columns", + "schema": "public", + "table": "IgStoryAutomation" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "repliesCount", + "entityType": "columns", + "schema": "public", + "table": "IgStoryAutomation" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{\"type\":\"all\",\"value\":[]}'", + "generated": null, + "identity": null, + "name": "story", + "entityType": "columns", + "schema": "public", + "table": "IgStoryAutomation" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{\"type\":\"none\",\"value\":null}'", + "generated": null, + "identity": null, + "name": "reply", + "entityType": "columns", + "schema": "public", + "table": "IgStoryAutomation" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{\"type\":\"all\",\"value\":[]}'", + "generated": null, + "identity": null, + "name": "includeKeywords", + "entityType": "columns", + "schema": "public", + "table": "IgStoryAutomation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Import" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Import" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Import" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "Import" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inboxId", + "entityType": "columns", + "schema": "public", + "table": "Import" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "Import" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fileId", + "entityType": "columns", + "schema": "public", + "table": "Import" + }, + { + "type": "importType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "Import" + }, + { + "type": "importFormat", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "format", + "entityType": "columns", + "schema": "public", + "table": "Import" + }, + { + "type": "importStatus", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "Import" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "meta", + "entityType": "columns", + "schema": "public", + "table": "Import" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "totalCount", + "entityType": "columns", + "schema": "public", + "table": "Import" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "processedCount", + "entityType": "columns", + "schema": "public", + "table": "Import" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "successCount", + "entityType": "columns", + "schema": "public", + "table": "Import" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "failedCount", + "entityType": "columns", + "schema": "public", + "table": "Import" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "errorMessage", + "entityType": "columns", + "schema": "public", + "table": "Import" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "errorSample", + "entityType": "columns", + "schema": "public", + "table": "Import" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "completedAt", + "entityType": "columns", + "schema": "public", + "table": "Import" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Inbox" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Inbox" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Inbox" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "Inbox" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "channel", + "entityType": "columns", + "schema": "public", + "table": "Inbox" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceId", + "entityType": "columns", + "schema": "public", + "table": "Inbox" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "Inbox" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'connected'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "Inbox" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "disconnectedAt", + "entityType": "columns", + "schema": "public", + "table": "Inbox" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "disconnectReason", + "entityType": "columns", + "schema": "public", + "table": "Inbox" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inboxId", + "entityType": "columns", + "schema": "public", + "table": "InboxContactStat" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "totalContacts", + "entityType": "columns", + "schema": "public", + "table": "InboxContactStat" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "InboxContactStat" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "InboxTeam" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "InboxTeam" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "InboxTeam" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "InboxTeam" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "InboxTeam" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "InboxTeamMember" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "InboxTeamMember" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "InboxTeamMember" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inboxTeamId", + "entityType": "columns", + "schema": "public", + "table": "InboxTeamMember" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "InboxTeamMember" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationActiveCampaign" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationActiveCampaign" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationActiveCampaign" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationActiveCampaign" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationActiveCampaign" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationActiveCampaign" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationApi" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationApi" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationApi" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationApi" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "IntegrationApi" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tokenHash", + "entityType": "columns", + "schema": "public", + "table": "IntegrationApi" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tokenPrefix", + "entityType": "columns", + "schema": "public", + "table": "IntegrationApi" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "callbackUrl", + "entityType": "columns", + "schema": "public", + "table": "IntegrationApi" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "enabled", + "entityType": "columns", + "schema": "public", + "table": "IntegrationApi" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationApi" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inboxId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationApi" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Integration" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Integration" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Integration" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "Integration" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationType", + "entityType": "columns", + "schema": "public", + "table": "Integration" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationClaude" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationClaude" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationClaude" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationClaude" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "autoReply", + "entityType": "columns", + "schema": "public", + "table": "IntegrationClaude" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationClaude" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationClaude" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "maxOutputTokens", + "entityType": "columns", + "schema": "public", + "table": "IntegrationClaude" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "model", + "entityType": "columns", + "schema": "public", + "table": "IntegrationClaude" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "prompt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationClaude" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "temperature", + "entityType": "columns", + "schema": "public", + "table": "IntegrationClaude" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationDeepseek" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationDeepseek" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationDeepseek" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationDeepseek" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "autoReply", + "entityType": "columns", + "schema": "public", + "table": "IntegrationDeepseek" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationDeepseek" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationDeepseek" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "maxOutputTokens", + "entityType": "columns", + "schema": "public", + "table": "IntegrationDeepseek" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "model", + "entityType": "columns", + "schema": "public", + "table": "IntegrationDeepseek" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "prompt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationDeepseek" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "temperature", + "entityType": "columns", + "schema": "public", + "table": "IntegrationDeepseek" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationDrip" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationDrip" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationDrip" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationDrip" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationDrip" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationDrip" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationFacebookAds" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationFacebookAds" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationFacebookAds" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationFacebookAds" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationFacebookAds" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationFacebookAds" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tokenExpiresAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationFacebookAds" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'active'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "IntegrationFacebookAds" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGemini" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGemini" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGemini" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGemini" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "autoReply", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGemini" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGemini" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGemini" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "maxOutputTokens", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGemini" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "model", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGemini" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "prompt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGemini" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "temperature", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGemini" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGetResponse" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGetResponse" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGetResponse" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGetResponse" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGetResponse" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGetResponse" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGoogleCalendar" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGoogleCalendar" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGoogleCalendar" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGoogleCalendar" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGoogleCalendar" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGoogleCalendar" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'primary'", + "generated": null, + "identity": null, + "name": "providerCalendarId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGoogleCalendar" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGoogleCalendar" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGoogleSheet" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGoogleSheet" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGoogleSheet" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGoogleSheet" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGoogleSheet" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGoogleSheet" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userInfo", + "entityType": "columns", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "igId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pageId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "username", + "entityType": "columns", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "coexistEnabled", + "entityType": "columns", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "coexistAiReadsSyncedHistory", + "entityType": "columns", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "hasCapiScope", + "entityType": "columns", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "capiScopeCheckedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "datasetId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "capiAccessToken", + "entityType": "columns", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "capiDisconnectedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "capiTestEventCode", + "entityType": "columns", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "", + "generated": null, + "identity": null, + "name": "conversationStarters", + "entityType": "columns", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "", + "generated": null, + "identity": null, + "name": "persistentMenus", + "entityType": "columns", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inboxId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "welcomeFlowId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'instagram'", + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tokenRefreshError", + "entityType": "columns", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationKlaviyo" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationKlaviyo" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationKlaviyo" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationKlaviyo" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationKlaviyo" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationKlaviyo" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMailchimp" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMailchimp" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMailchimp" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMailchimp" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMailchimp" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMailchimp" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMailerLite" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMailerLite" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMailerLite" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMailerLite" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMailerLite" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMailerLite" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userInfo", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pageId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "", + "generated": null, + "identity": null, + "name": "conversationStarters", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "", + "generated": null, + "identity": null, + "name": "persistentMenus", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "", + "generated": null, + "identity": null, + "name": "personas", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "personaId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "coexistEnabled", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "coexistAiReadsSyncedHistory", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "hasCapiScope", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "capiScopeCheckedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "datasetId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "capiAccessToken", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "capiDisconnectedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "capiTestEventCode", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inboxId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "welcomeFlowId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "syncTagEnabledAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tokenRefreshError", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "catalogId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "catalogName", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "businessId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "encryptedAuth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "type": "metaCatalogAuthMode", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'oauth'", + "generated": null, + "identity": null, + "name": "authMode", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tokenExpiresAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "type": "metaCatalogConnectionStatus", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'active'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deletedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "type": "metaCatalogImportStatus", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'idle'", + "generated": null, + "identity": null, + "name": "importStatus", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "importTotalCount", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "importedCount", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "importFailedCount", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "importError", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastImportedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'VND'", + "generated": null, + "identity": null, + "name": "currency", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "storeUrl", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMoosend" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMoosend" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMoosend" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMoosend" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMoosend" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMoosend" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenai" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenai" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenai" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenai" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "autoReply", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenai" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "autoReplyVoice", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenai" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "voice", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenai" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "prompt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenai" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "model", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenai" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "temperature", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenai" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "maxOutputTokens", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenai" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenai" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenai" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "aiAgentId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenai" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenaiCompatible" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenaiCompatible" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenaiCompatible" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenaiCompatible" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "autoReply", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenaiCompatible" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "baseURL", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenaiCompatible" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "defaultModel", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenaiCompatible" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "enabled", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenaiCompatible" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenaiCompatible" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenaiCompatible" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "preset", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenaiCompatible" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenaiCompatible" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenrouter" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenrouter" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenrouter" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenrouter" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "autoReply", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenrouter" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenrouter" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenrouter" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "maxOutputTokens", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenrouter" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "model", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenrouter" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "prompt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenrouter" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "temperature", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenrouter" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOutlookCalendar" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOutlookCalendar" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOutlookCalendar" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOutlookCalendar" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOutlookCalendar" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOutlookCalendar" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'primary'", + "generated": null, + "identity": null, + "name": "providerCalendarId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOutlookCalendar" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOutlookCalendar" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationSendGrid" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationSendGrid" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationSendGrid" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationSendGrid" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationSendGrid" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationSendGrid" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationSmtp" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationSmtp" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationSmtp" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationSmtp" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "IntegrationSmtp" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fromAddress", + "entityType": "columns", + "schema": "public", + "table": "IntegrationSmtp" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationSmtp" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inboxId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationSmtp" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationTelegram" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationTelegram" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationTelegram" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationTelegram" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "botId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationTelegram" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "IntegrationTelegram" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationTelegram" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inboxId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationTelegram" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationTiktok" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationTiktok" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationTiktok" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationTiktok" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "openId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationTiktok" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "IntegrationTiktok" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationTiktok" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inboxId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationTiktok" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tokenRefreshError", + "entityType": "columns", + "schema": "public", + "table": "IntegrationTiktok" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWebchat" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWebchat" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWebchat" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWebchat" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWebchat" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "enable", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWebchat" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "", + "generated": null, + "identity": null, + "name": "authorizedDomains", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWebchat" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "", + "generated": null, + "identity": null, + "name": "conversationStarters", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWebchat" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "", + "generated": null, + "identity": null, + "name": "persistentMenus", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWebchat" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "brandColor", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWebchat" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "hideHeader", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWebchat" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "showLogo", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWebchat" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "hideMessageInput", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWebchat" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "customCss", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWebchat" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWebchat" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inboxId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWebchat" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "welcomeFlowId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWebchat" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "phoneNumberId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "wabaId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "businessId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "displayPhoneNumber", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "coexistEnabled", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "coexistAiReadsSyncedHistory", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isCoexist", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "platformType", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "historyDeclined", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "hasCapiScope", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "capiScopeCheckedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "datasetId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "capiAccessToken", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "capiDisconnectedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "capiTestEventCode", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "whatsappRegistrationStatus", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'pending_verification'", + "generated": null, + "identity": null, + "name": "registrationStatus", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "registrationError", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "verificationCodeRequestedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tokenRefreshError", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inboxId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationZalo" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationZalo" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationZalo" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationZalo" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "oaId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationZalo" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "IntegrationZalo" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationZalo" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inboxId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationZalo" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fallbackFlowId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationZalo" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "syncTagEnabledAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationZalo" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tokenRefreshError", + "entityType": "columns", + "schema": "public", + "table": "IntegrationZalo" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "MagicLink" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "MagicLink" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "MagicLink" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "MagicLink" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "MagicLink" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "url", + "entityType": "columns", + "schema": "public", + "table": "MagicLink" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "MagicLinkStat" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "linkId", + "entityType": "columns", + "schema": "public", + "table": "MagicLinkStat" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "MagicLinkStat" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactInboxId", + "entityType": "columns", + "schema": "public", + "table": "MagicLinkStat" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "occurredAt", + "entityType": "columns", + "schema": "public", + "table": "MagicLinkStat" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "MagicLinkStat" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "MediaLibraryFile" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "MediaLibraryFile" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "MediaLibraryFile" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "MediaLibraryFile" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "path", + "entityType": "columns", + "schema": "public", + "table": "MediaLibraryFile" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "mimeType", + "entityType": "columns", + "schema": "public", + "table": "MediaLibraryFile" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "size", + "entityType": "columns", + "schema": "public", + "table": "MediaLibraryFile" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isFavourite", + "entityType": "columns", + "schema": "public", + "table": "MediaLibraryFile" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastAccessedAt", + "entityType": "columns", + "schema": "public", + "table": "MediaLibraryFile" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "MediaLibraryFile" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "folderId", + "entityType": "columns", + "schema": "public", + "table": "MediaLibraryFile" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "MediaLibraryFolder" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "MediaLibraryFolder" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "MediaLibraryFolder" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "MediaLibraryFolder" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "MediaLibraryFolder" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Message" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Message" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Message" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "conversationId", + "entityType": "columns", + "schema": "public", + "table": "Message" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactInboxId", + "entityType": "columns", + "schema": "public", + "table": "Message" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "Message" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "text", + "entityType": "columns", + "schema": "public", + "table": "Message" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contentAttributes", + "entityType": "columns", + "schema": "public", + "table": "Message" + }, + { + "type": "messageType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "messageType", + "entityType": "columns", + "schema": "public", + "table": "Message" + }, + { + "type": "contentType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contentType", + "entityType": "columns", + "schema": "public", + "table": "Message" + }, + { + "type": "senderType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "senderType", + "entityType": "columns", + "schema": "public", + "table": "Message" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "senderId", + "entityType": "columns", + "schema": "public", + "table": "Message" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceId", + "entityType": "columns", + "schema": "public", + "table": "Message" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deletedAt", + "entityType": "columns", + "schema": "public", + "table": "Message" + }, + { + "type": "messageKind", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'message'", + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "Message" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "parentId", + "entityType": "columns", + "schema": "public", + "table": "Message" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "attributes", + "entityType": "columns", + "schema": "public", + "table": "Message" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sendError", + "entityType": "columns", + "schema": "public", + "table": "Message" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "MessageCleanup" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "MessageCleanup" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "MessageCleanup" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "MessageCleanup" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "MessageCleanup" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactInboxId", + "entityType": "columns", + "schema": "public", + "table": "MessageCleanup" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inboxId", + "entityType": "columns", + "schema": "public", + "table": "MessageCleanup" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceId", + "entityType": "columns", + "schema": "public", + "table": "MessageCleanup" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "conversationIds", + "entityType": "columns", + "schema": "public", + "table": "MessageCleanup" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sinceTime", + "entityType": "columns", + "schema": "public", + "table": "MessageCleanup" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "deletedAt", + "entityType": "columns", + "schema": "public", + "table": "MessageCleanup" + }, + { + "type": "MessageCleanupStatus", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "MessageCleanup" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "attempts", + "entityType": "columns", + "schema": "public", + "table": "MessageCleanup" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastError", + "entityType": "columns", + "schema": "public", + "table": "MessageCleanup" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "processedAt", + "entityType": "columns", + "schema": "public", + "table": "MessageCleanup" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "type": "messagingAdChannel", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "channel", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationWhatsappId", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationMessengerId", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationInstagramId", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "adAccountId", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "type": "messagingAdCreateState", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "createState", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "type": "messagingAdPublishState", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "publishState", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "metaCampaignId", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "metaAdSetId", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "metaAdCreativeId", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "metaAdId", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "input", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastError", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "cleanupError", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "createdBy", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdsConnection" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdsConnection" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdsConnection" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdsConnection" + }, + { + "type": "messagingAdChannel", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "channel", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdsConnection" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationWhatsappId", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdsConnection" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationMessengerId", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdsConnection" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationInstagramId", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdsConnection" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdsConnection" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'active'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdsConnection" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "MessengerMessageTemplate" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "MessengerMessageTemplate" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "MessengerMessageTemplate" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "MessengerMessageTemplate" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationMessengerId", + "entityType": "columns", + "schema": "public", + "table": "MessengerMessageTemplate" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceId", + "entityType": "columns", + "schema": "public", + "table": "MessengerMessageTemplate" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "language", + "entityType": "columns", + "schema": "public", + "table": "MessengerMessageTemplate" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "category", + "entityType": "columns", + "schema": "public", + "table": "MessengerMessageTemplate" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "MessengerMessageTemplate" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'POSITIONAL'", + "generated": null, + "identity": null, + "name": "parameterFormat", + "entityType": "columns", + "schema": "public", + "table": "MessengerMessageTemplate" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "components", + "entityType": "columns", + "schema": "public", + "table": "MessengerMessageTemplate" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "channel", + "entityType": "columns", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationId", + "entityType": "columns", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactInboxId", + "entityType": "columns", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "eventName", + "entityType": "columns", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "currency", + "entityType": "columns", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contentCategory", + "entityType": "columns", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contentName", + "entityType": "columns", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'business_messaging'", + "generated": null, + "identity": null, + "name": "actionSource", + "entityType": "columns", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contentType", + "entityType": "columns", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contentIds", + "entityType": "columns", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "source", + "entityType": "columns", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceKey", + "entityType": "columns", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "occurredAt", + "entityType": "columns", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "capiStatus", + "entityType": "columns", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "capiSentAt", + "entityType": "columns", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "capiError", + "entityType": "columns", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogItem" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogItem" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogItem" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationMetaCatalogId", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogItem" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "productId", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogItem" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "catalogId", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogItem" + }, + { + "type": "metaCatalogItemDirection", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'push'", + "generated": null, + "identity": null, + "name": "direction", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogItem" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "retailerId", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogItem" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastSyncedFingerprint", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogItem" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastSyncedAt", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogItem" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationMetaCatalogId", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "type": "metaCatalogSyncStatus", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'queued'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "type": "metaCatalogItemDirection", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'push'", + "generated": null, + "identity": null, + "name": "direction", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "catalogId", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "type": "metaCatalogSyncScope", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'all'", + "generated": null, + "identity": null, + "name": "scope", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "categoryId", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "selectedProductIds", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "handles", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "submissionLeaseId", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "totalCount", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "succeededCount", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "failedCount", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "skippedCount", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "itemErrors", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "skippedItems", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "pollAttempt", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "error", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "startedAt", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "finishedAt", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Minigame" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Minigame" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Minigame" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "Minigame" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "Minigame" + }, + { + "type": "minigameType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "Minigame" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "enabled", + "entityType": "columns", + "schema": "public", + "table": "Minigame" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "generalSettings", + "entityType": "columns", + "schema": "public", + "table": "Minigame" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "appearance", + "entityType": "columns", + "schema": "public", + "table": "Minigame" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "playerSettings", + "entityType": "columns", + "schema": "public", + "table": "Minigame" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "prizeSettings", + "entityType": "columns", + "schema": "public", + "table": "Minigame" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "winningMessageSettings", + "entityType": "columns", + "schema": "public", + "table": "Minigame" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "nonWinningMessageSettings", + "entityType": "columns", + "schema": "public", + "table": "Minigame" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "playsCount", + "entityType": "columns", + "schema": "public", + "table": "Minigame" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "participantsCount", + "entityType": "columns", + "schema": "public", + "table": "Minigame" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "winnersCount", + "entityType": "columns", + "schema": "public", + "table": "Minigame" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "sharesCount", + "entityType": "columns", + "schema": "public", + "table": "Minigame" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "MinigameContact" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "MinigameContact" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "MinigameContact" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "minigameId", + "entityType": "columns", + "schema": "public", + "table": "MinigameContact" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "MinigameContact" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "openedAt", + "entityType": "columns", + "schema": "public", + "table": "MinigameContact" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "played", + "entityType": "columns", + "schema": "public", + "table": "MinigameContact" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "remaining", + "entityType": "columns", + "schema": "public", + "table": "MinigameContact" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "sharesCount", + "entityType": "columns", + "schema": "public", + "table": "MinigameContact" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "referrerContactId", + "entityType": "columns", + "schema": "public", + "table": "MinigameContact" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactInboxId", + "entityType": "columns", + "schema": "public", + "table": "MinigameContact" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "MinigamePlay" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "MinigamePlay" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "MinigamePlay" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "minigameId", + "entityType": "columns", + "schema": "public", + "table": "MinigamePlay" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "MinigamePlay" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "isWinning", + "entityType": "columns", + "schema": "public", + "table": "MinigamePlay" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "prizeId", + "entityType": "columns", + "schema": "public", + "table": "MinigamePlay" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "prizeName", + "entityType": "columns", + "schema": "public", + "table": "MinigamePlay" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "PlatformCredential" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "PlatformCredential" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "PlatformCredential" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "PlatformCredential" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "PlatformCredential" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publicConfig", + "entityType": "columns", + "schema": "public", + "table": "PlatformCredential" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "public", + "table": "PlatformCredential" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "livemode", + "entityType": "columns", + "schema": "public", + "table": "PlatformCredential" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "usePlatformCredential", + "entityType": "columns", + "schema": "public", + "table": "PlatformCredential" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isVerified", + "entityType": "columns", + "schema": "public", + "table": "PlatformCredential" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "verifiedAt", + "entityType": "columns", + "schema": "public", + "table": "PlatformCredential" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastUsedAt", + "entityType": "columns", + "schema": "public", + "table": "PlatformCredential" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "shortDescription", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "longDescription", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "price", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "taxes", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "discount", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'USD'", + "generated": null, + "identity": null, + "name": "currency", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "productUrl", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sku", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "inventoryPolicy", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'dont_track'", + "generated": null, + "identity": null, + "name": "inventoryPolicy", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "inventoryQuantity", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "allowOutOfStockPurchase", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "images", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "tags", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "vendor", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "10", + "generated": null, + "identity": null, + "name": "rank", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "categoryId", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "subcategoryId", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "isActive", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "isSearchable", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "allowSpecialRequest", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isAddonOnly", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "ProductAddon" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "ProductAddon" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "ProductAddon" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "productId", + "entityType": "columns", + "schema": "public", + "table": "ProductAddon" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "ProductAddon" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "maxSelections", + "entityType": "columns", + "schema": "public", + "table": "ProductAddon" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "addonProductIds", + "entityType": "columns", + "schema": "public", + "table": "ProductAddon" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "ProductCategory" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "ProductCategory" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "ProductCategory" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "ProductCategory" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "parentId", + "entityType": "columns", + "schema": "public", + "table": "ProductCategory" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "ProductCategory" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "10", + "generated": null, + "identity": null, + "name": "rank", + "entityType": "columns", + "schema": "public", + "table": "ProductCategory" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "ProductVariant" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "ProductVariant" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "ProductVariant" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "productId", + "entityType": "columns", + "schema": "public", + "table": "ProductVariant" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "combination", + "entityType": "columns", + "schema": "public", + "table": "ProductVariant" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "price", + "entityType": "columns", + "schema": "public", + "table": "ProductVariant" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "isEnabled", + "entityType": "columns", + "schema": "public", + "table": "ProductVariant" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "ProductVariantOption" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "ProductVariantOption" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "ProductVariantOption" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "productId", + "entityType": "columns", + "schema": "public", + "table": "ProductVariantOption" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "ProductVariantOption" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "values", + "entityType": "columns", + "schema": "public", + "table": "ProductVariantOption" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "10", + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "ProductVariantOption" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireAnswer" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireAnswer" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireAnswer" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "submissionId", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireAnswer" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "questionId", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireAnswer" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "questionIdSnapshot", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireAnswer" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "questionTitleSnapshot", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireAnswer" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "questionTypeSnapshot", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireAnswer" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "labelSnapshot", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireAnswer" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireAnswer" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pointsEarned", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireAnswer" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "attemptCount", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireAnswer" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "answeredAt", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireAnswer" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Questionnaire" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Questionnaire" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Questionnaire" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "Questionnaire" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "enableScore", + "entityType": "columns", + "schema": "public", + "table": "Questionnaire" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "enableRetryMessages", + "entityType": "columns", + "schema": "public", + "table": "Questionnaire" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "enableCustomFieldMapping", + "entityType": "columns", + "schema": "public", + "table": "Questionnaire" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deletedAt", + "entityType": "columns", + "schema": "public", + "table": "Questionnaire" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "triggerFlowId", + "entityType": "columns", + "schema": "public", + "table": "Questionnaire" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "Questionnaire" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireQuestion" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireQuestion" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireQuestion" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "questionnaireId", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireQuestion" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireQuestion" + }, + { + "type": "questionnaireQuestionType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireQuestion" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "active", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireQuestion" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "image", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireQuestion" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "orderNo", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireQuestion" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "point", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireQuestion" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "retryMessage", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireQuestion" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "customFieldId", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireQuestion" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "systemFieldKey", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireQuestion" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "config", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireQuestion" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deletedAt", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireQuestion" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireSubmission" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireSubmission" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireSubmission" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireSubmission" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "questionnaireId", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireSubmission" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireSubmission" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "conversationId", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireSubmission" + }, + { + "type": "questionnaireSubmissionStatus", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'inProgress'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireSubmission" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "totalPoints", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireSubmission" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "currentQuestionId", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireSubmission" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "currentQuestionSentAt", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireSubmission" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastAnsweredMessageId", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireSubmission" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "startedAt", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireSubmission" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "completedAt", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireSubmission" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "cancelledAt", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireSubmission" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "RefLinkStat" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "linkId", + "entityType": "columns", + "schema": "public", + "table": "RefLinkStat" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "RefLinkStat" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactInboxId", + "entityType": "columns", + "schema": "public", + "table": "RefLinkStat" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "occurredAt", + "entityType": "columns", + "schema": "public", + "table": "RefLinkStat" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "RefLinkStat" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Reflink" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Reflink" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Reflink" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "Reflink" + }, + { + "type": "ReflinkType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'refLink'", + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "Reflink" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "flowId", + "entityType": "columns", + "schema": "public", + "table": "Reflink" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "Reflink" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "customFieldId", + "entityType": "columns", + "schema": "public", + "table": "Reflink" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "qrStyles", + "entityType": "columns", + "schema": "public", + "table": "Reflink" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "SavedReply" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "SavedReply" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "SavedReply" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "shortcut", + "entityType": "columns", + "schema": "public", + "table": "SavedReply" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "text", + "entityType": "columns", + "schema": "public", + "table": "SavedReply" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "SavedReply" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Sequence" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Sequence" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Sequence" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "Sequence" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "folderId", + "entityType": "columns", + "schema": "public", + "table": "Sequence" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "active", + "entityType": "columns", + "schema": "public", + "table": "Sequence" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "subscribers", + "entityType": "columns", + "schema": "public", + "table": "Sequence" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "messages", + "entityType": "columns", + "schema": "public", + "table": "Sequence" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "Sequence" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "runAtMs", + "entityType": "columns", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "bucket", + "entityType": "columns", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "idempotencyKey", + "entityType": "columns", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "attempt", + "entityType": "columns", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastError", + "entityType": "columns", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lockedAt", + "entityType": "columns", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lockOwner", + "entityType": "columns", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "completedAt", + "entityType": "columns", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deliveredAt", + "entityType": "columns", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "seenAt", + "entityType": "columns", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "clickedAt", + "entityType": "columns", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "failedAt", + "entityType": "columns", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "errorContent", + "entityType": "columns", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sequenceId", + "entityType": "columns", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactInboxId", + "entityType": "columns", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "stepId", + "entityType": "columns", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "enrollmentId", + "entityType": "columns", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": { + "as": "case when \"seenAt\" is null then false when \"deliveredAt\" is null then false else \"seenAt\" >= \"deliveredAt\" end", + "type": "stored" + }, + "identity": null, + "name": "isRead", + "entityType": "columns", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "SequenceStep" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "SequenceStep" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "SequenceStep" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "order", + "entityType": "columns", + "schema": "public", + "table": "SequenceStep" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "delayDays", + "entityType": "columns", + "schema": "public", + "table": "SequenceStep" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "delayMinutes", + "entityType": "columns", + "schema": "public", + "table": "SequenceStep" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "delayUnit", + "entityType": "columns", + "schema": "public", + "table": "SequenceStep" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "specificDateTime", + "entityType": "columns", + "schema": "public", + "table": "SequenceStep" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "isActive", + "entityType": "columns", + "schema": "public", + "table": "SequenceStep" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "anytime", + "entityType": "columns", + "schema": "public", + "table": "SequenceStep" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sendTimeStart", + "entityType": "columns", + "schema": "public", + "table": "SequenceStep" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sendTimeEnd", + "entityType": "columns", + "schema": "public", + "table": "SequenceStep" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "'[\"monday\",\"tuesday\",\"wednesday\",\"thursday\",\"friday\",\"saturday\",\"sunday\"]'", + "generated": null, + "identity": null, + "name": "sendDays", + "entityType": "columns", + "schema": "public", + "table": "SequenceStep" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "flowId", + "entityType": "columns", + "schema": "public", + "table": "SequenceStep" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sequenceId", + "entityType": "columns", + "schema": "public", + "table": "SequenceStep" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Spreadsheet" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Spreadsheet" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Spreadsheet" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "Spreadsheet" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "Spreadsheet" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "url", + "entityType": "columns", + "schema": "public", + "table": "Spreadsheet" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "spreadsheetId", + "entityType": "columns", + "schema": "public", + "table": "Spreadsheet" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "SystemField" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "SystemField" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "SystemField" + }, + { + "type": "SystemFieldType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "SystemField" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "payload", + "entityType": "columns", + "schema": "public", + "table": "SystemField" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Tag" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Tag" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Tag" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "Tag" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deletedAt", + "entityType": "columns", + "schema": "public", + "table": "Tag" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "folderId", + "entityType": "columns", + "schema": "public", + "table": "Tag" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "Tag" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "TagChannel" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "TagChannel" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "TagChannel" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "TagChannel" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tagId", + "entityType": "columns", + "schema": "public", + "table": "TagChannel" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "channelType", + "entityType": "columns", + "schema": "public", + "table": "TagChannel" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationId", + "entityType": "columns", + "schema": "public", + "table": "TagChannel" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "externalLabelId", + "entityType": "columns", + "schema": "public", + "table": "TagChannel" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Template" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Template" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Template" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "Template" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tenantId", + "entityType": "columns", + "schema": "public", + "table": "Template" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "Template" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "Template" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "imageUrl", + "entityType": "columns", + "schema": "public", + "table": "Template" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publisherName", + "entityType": "columns", + "schema": "public", + "table": "Template" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "youtubeVideoId", + "entityType": "columns", + "schema": "public", + "table": "Template" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "testLink", + "entityType": "columns", + "schema": "public", + "table": "Template" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "selection", + "entityType": "columns", + "schema": "public", + "table": "Template" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "payload", + "entityType": "columns", + "schema": "public", + "table": "Template" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "categoryCounts", + "entityType": "columns", + "schema": "public", + "table": "Template" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "formatVersion", + "entityType": "columns", + "schema": "public", + "table": "Template" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "shareToken", + "entityType": "columns", + "schema": "public", + "table": "Template" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "shareEnabled", + "entityType": "columns", + "schema": "public", + "table": "Template" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "shareExpiresAt", + "entityType": "columns", + "schema": "public", + "table": "Template" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "defaultPermissions", + "entityType": "columns", + "schema": "public", + "table": "Template" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "createInstallFolder", + "entityType": "columns", + "schema": "public", + "table": "Template" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "defaultAutoUpdate", + "entityType": "columns", + "schema": "public", + "table": "Template" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "installCount", + "entityType": "columns", + "schema": "public", + "table": "Template" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "createdBy", + "entityType": "columns", + "schema": "public", + "table": "Template" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deletedAt", + "entityType": "columns", + "schema": "public", + "table": "Template" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "templateId", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "templateName", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceWorkspaceId", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "formatVersion", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "type": "templateInstallationStatus", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "permissions", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "warnings", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "warningCount", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "errorMessage", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "resourceCount", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "installFolderId", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "autoUpdate", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceUpdatedAt", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "installedBy", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "completedAt", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstalledResource" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstalledResource" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstalledResource" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "installationId", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstalledResource" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstalledResource" + }, + { + "type": "templateResourceCategory", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "category", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstalledResource" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "resourceKind", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstalledResource" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "resourceId", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstalledResource" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceResourceId", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstalledResource" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "wasExisting", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstalledResource" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Trigger" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Trigger" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Trigger" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "Trigger" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "active", + "entityType": "columns", + "schema": "public", + "table": "Trigger" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "folderId", + "entityType": "columns", + "schema": "public", + "table": "Trigger" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "", + "generated": null, + "identity": null, + "name": "actions", + "entityType": "columns", + "schema": "public", + "table": "Trigger" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "Trigger" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Condition" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Condition" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Condition" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "triggerId", + "entityType": "columns", + "schema": "public", + "table": "Condition" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "webhookId", + "entityType": "columns", + "schema": "public", + "table": "Condition" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "Condition" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceId", + "entityType": "columns", + "schema": "public", + "table": "Condition" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "operator", + "entityType": "columns", + "schema": "public", + "table": "Condition" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "public", + "table": "Condition" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "TriggerContactHistory" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "TriggerContactHistory" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "TriggerContactHistory" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "triggerId", + "entityType": "columns", + "schema": "public", + "table": "TriggerContactHistory" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "TriggerContactHistory" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "TriggerContactHistory" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "firstEnteredAt", + "entityType": "columns", + "schema": "public", + "table": "TriggerContactHistory" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "TriggerExecution" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "TriggerExecution" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "TriggerExecution" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "executedAt", + "entityType": "columns", + "schema": "public", + "table": "TriggerExecution" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "triggerId", + "entityType": "columns", + "schema": "public", + "table": "TriggerExecution" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "TriggerExecution" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "TriggerExecution" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "TriggerStat" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "TriggerStat" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "TriggerStat" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "triggerId", + "entityType": "columns", + "schema": "public", + "table": "TriggerStat" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "TriggerStat" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "date", + "entityType": "columns", + "schema": "public", + "table": "TriggerStat" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "totalContacts", + "entityType": "columns", + "schema": "public", + "table": "TriggerStat" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "successCount", + "entityType": "columns", + "schema": "public", + "table": "TriggerStat" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "failureCount", + "entityType": "columns", + "schema": "public", + "table": "TriggerStat" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "totalExecutions", + "entityType": "columns", + "schema": "public", + "table": "TriggerStat" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "UserDeviceToken" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "UserDeviceToken" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "UserDeviceToken" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "UserDeviceToken" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "UserDeviceToken" + }, + { + "type": "devicePlatform", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "platform", + "entityType": "columns", + "schema": "public", + "table": "UserDeviceToken" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "public", + "table": "UserDeviceToken" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "lastSeenAt", + "entityType": "columns", + "schema": "public", + "table": "UserDeviceToken" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "UserPersistentMenu" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "UserPersistentMenu" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "UserPersistentMenu" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "UserPersistentMenu" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "", + "generated": null, + "identity": null, + "name": "menus", + "entityType": "columns", + "schema": "public", + "table": "UserPersistentMenu" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "UserPersistentMenu" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Webhook" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Webhook" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Webhook" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "Webhook" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "active", + "entityType": "columns", + "schema": "public", + "table": "Webhook" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "folderId", + "entityType": "columns", + "schema": "public", + "table": "Webhook" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "url", + "entityType": "columns", + "schema": "public", + "table": "Webhook" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "Webhook" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "WebhookExecution" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "WebhookExecution" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "WebhookExecution" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "executedAt", + "entityType": "columns", + "schema": "public", + "table": "WebhookExecution" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "webhookId", + "entityType": "columns", + "schema": "public", + "table": "WebhookExecution" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "WebhookExecution" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "WebhookExecution" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "WhatsappBusinessAccount" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "WhatsappBusinessAccount" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "WhatsappBusinessAccount" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "WhatsappBusinessAccount" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "wabaId", + "entityType": "columns", + "schema": "public", + "table": "WhatsappBusinessAccount" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "businessId", + "entityType": "columns", + "schema": "public", + "table": "WhatsappBusinessAccount" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "credential", + "entityType": "columns", + "schema": "public", + "table": "WhatsappBusinessAccount" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "grantedScopes", + "entityType": "columns", + "schema": "public", + "table": "WhatsappBusinessAccount" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "scopeCheckedAt", + "entityType": "columns", + "schema": "public", + "table": "WhatsappBusinessAccount" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provisionedAt", + "entityType": "columns", + "schema": "public", + "table": "WhatsappBusinessAccount" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "creditLineId", + "entityType": "columns", + "schema": "public", + "table": "WhatsappBusinessAccount" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "revision", + "entityType": "columns", + "schema": "public", + "table": "WhatsappBusinessAccount" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "WhatsappCoexistStaging" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "WhatsappCoexistStaging" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "WhatsappCoexistStaging" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "phoneNumberId", + "entityType": "columns", + "schema": "public", + "table": "WhatsappCoexistStaging" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "payload", + "entityType": "columns", + "schema": "public", + "table": "WhatsappCoexistStaging" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "payloadHash", + "entityType": "columns", + "schema": "public", + "table": "WhatsappCoexistStaging" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "processedAt", + "entityType": "columns", + "schema": "public", + "table": "WhatsappCoexistStaging" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "parseFailedAt", + "entityType": "columns", + "schema": "public", + "table": "WhatsappCoexistStaging" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "WhatsappFlow" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "WhatsappFlow" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "WhatsappFlow" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "WhatsappFlow" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationWhatsappId", + "entityType": "columns", + "schema": "public", + "table": "WhatsappFlow" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceId", + "entityType": "columns", + "schema": "public", + "table": "WhatsappFlow" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "WhatsappFlow" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "categories", + "entityType": "columns", + "schema": "public", + "table": "WhatsappFlow" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "validationErrors", + "entityType": "columns", + "schema": "public", + "table": "WhatsappFlow" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "completedCount", + "entityType": "columns", + "schema": "public", + "table": "WhatsappFlow" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "screens", + "entityType": "columns", + "schema": "public", + "table": "WhatsappFlow" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "WhatsappMessageTemplate" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "WhatsappMessageTemplate" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "WhatsappMessageTemplate" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "WhatsappMessageTemplate" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationWhatsappId", + "entityType": "columns", + "schema": "public", + "table": "WhatsappMessageTemplate" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceId", + "entityType": "columns", + "schema": "public", + "table": "WhatsappMessageTemplate" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "language", + "entityType": "columns", + "schema": "public", + "table": "WhatsappMessageTemplate" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "category", + "entityType": "columns", + "schema": "public", + "table": "WhatsappMessageTemplate" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "WhatsappMessageTemplate" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "components", + "entityType": "columns", + "schema": "public", + "table": "WhatsappMessageTemplate" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "WhatsappSignupSession" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "WhatsappSignupSession" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "WhatsappSignupSession" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "WhatsappSignupSession" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ownerId", + "entityType": "columns", + "schema": "public", + "table": "WhatsappSignupSession" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "WhatsappSignupSession" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "wabaId", + "entityType": "columns", + "schema": "public", + "table": "WhatsappSignupSession" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "businessId", + "entityType": "columns", + "schema": "public", + "table": "WhatsappSignupSession" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "encryptedAccessToken", + "entityType": "columns", + "schema": "public", + "table": "WhatsappSignupSession" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "apiVersion", + "entityType": "columns", + "schema": "public", + "table": "WhatsappSignupSession" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": null, + "generated": null, + "identity": null, + "name": "candidatePhoneNumberIds", + "entityType": "columns", + "schema": "public", + "table": "WhatsappSignupSession" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "claimedPhoneNumberIds", + "entityType": "columns", + "schema": "public", + "table": "WhatsappSignupSession" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "WhatsappSignupSession" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "consumedAt", + "entityType": "columns", + "schema": "public", + "table": "WhatsappSignupSession" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Workspace" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Workspace" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Workspace" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "Workspace" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "defaultReply", + "entityType": "columns", + "schema": "public", + "table": "Workspace" + }, + { + "type": "defaultReplyFrequency", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'allTime'", + "generated": null, + "identity": null, + "name": "defaultReplyFrequency", + "entityType": "columns", + "schema": "public", + "table": "Workspace" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "targetCountry", + "entityType": "columns", + "schema": "public", + "table": "Workspace" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'en'", + "generated": null, + "identity": null, + "name": "language", + "entityType": "columns", + "schema": "public", + "table": "Workspace" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'UTC'", + "generated": null, + "identity": null, + "name": "timezone", + "entityType": "columns", + "schema": "public", + "table": "Workspace" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'#016DFF'", + "generated": null, + "identity": null, + "name": "brandColor", + "entityType": "columns", + "schema": "public", + "table": "Workspace" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "developmentMode", + "entityType": "columns", + "schema": "public", + "table": "Workspace" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "smartResponseDelaySeconds", + "entityType": "columns", + "schema": "public", + "table": "Workspace" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "isActive", + "entityType": "columns", + "schema": "public", + "table": "Workspace" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "startTime", + "entityType": "columns", + "schema": "public", + "table": "Workspace" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "endTime", + "entityType": "columns", + "schema": "public", + "table": "Workspace" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "logo", + "entityType": "columns", + "schema": "public", + "table": "Workspace" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "scheduledDeletionAt", + "entityType": "columns", + "schema": "public", + "table": "Workspace" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "supportAccessUntil", + "entityType": "columns", + "schema": "public", + "table": "Workspace" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "capiLimitedDataUse", + "entityType": "columns", + "schema": "public", + "table": "Workspace" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "public", + "table": "Workspace" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ownerId", + "entityType": "columns", + "schema": "public", + "table": "Workspace" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "tenantId", + "entityType": "columns", + "schema": "public", + "table": "Workspace" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceApiToken" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceApiToken" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceApiToken" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceApiToken" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceApiToken" + }, + { + "type": "WorkspaceApiTokenPermission", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "permission", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceApiToken" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tokenHash", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceApiToken" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tokenPrefix", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceApiToken" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isDefault", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceApiToken" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "encryptedToken", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceApiToken" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 1, + "default": null, + "generated": null, + "identity": null, + "name": "scopes", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceApiToken" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceMac" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceMac" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceMac" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceMac" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "periodStart", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceMac" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "periodEnd", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceMac" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "macCount", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceMac" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceMember" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceMember" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceMember" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceMember" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceMember" + }, + { + "type": "workspaceMemberRole", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceMember" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "notificationChannels", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceMember" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "notificationTypes", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceMember" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "permissions", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceMember" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "sentCount", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "deliveredCount", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "seenCount", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "clickedCount", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "failedCount", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactInboxId", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deliveredAt", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "seenAt", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "clickedAt", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "failedAt", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "missedCount", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationMiss" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationMiss" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationMiss" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationMiss" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "automationId", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationMiss" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationMiss" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactInboxId", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationMiss" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "postId", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationMiss" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "commentId", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationMiss" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "commentText", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationMiss" + }, + { + "type": "commentAutomationMissReason", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reason", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationMiss" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "occurredAt", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationMiss" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "isActive", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MessageShard_isActive_idx", + "entityType": "indexes", + "schema": "public", + "table": "MessageShard" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "shardKey", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MessageShard_shardKey_idx", + "entityType": "indexes", + "schema": "public", + "table": "MessageShard" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "startTime", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "endTime", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ShardTimeRange_time_lookup_idx", + "entityType": "indexes", + "schema": "public", + "table": "ShardTimeRange" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "shardId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ShardTimeRange_shardId_idx", + "entityType": "indexes", + "schema": "public", + "table": "ShardTimeRange" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "integrationWhatsappId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "source", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "sourceEventId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AdsConversionEvent_workspace_integration_source_sourceEventId_key", + "entityType": "indexes", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "integrationWhatsappId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "source", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "sourceEventId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"channel\" = 'whatsapp'", + "with": "", + "method": "btree", + "concurrently": false, + "name": "AdsConversionEvent_ws_whatsapp_source_sourceEventId_key", + "entityType": "indexes", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "integrationMessengerId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "source", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "sourceEventId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"channel\" = 'messenger'", + "with": "", + "method": "btree", + "concurrently": false, + "name": "AdsConversionEvent_ws_messenger_source_sourceEventId_key", + "entityType": "indexes", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "integrationInstagramId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "source", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "sourceEventId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"channel\" = 'instagram'", + "with": "", + "method": "btree", + "concurrently": false, + "name": "AdsConversionEvent_ws_instagram_source_sourceEventId_key", + "entityType": "indexes", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "eventType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "occurredAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AdsConversionEvent_workspaceId_eventType_occurredAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactInboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AdsConversionEvent_contactInboxId_idx", + "entityType": "indexes", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "occurredAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AdsConversionEvent_workspaceId_occurredAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "adId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "occurredAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AdsConversionEvent_workspaceId_adId_occurredAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "channel", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "enabled", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AdsConversionRule_workspaceId_channel_enabled_idx", + "entityType": "indexes", + "schema": "public", + "table": "AdsConversionRule" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "sourceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AIConversationEmbedding_sourceId_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "AIConversationEmbedding" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "sourceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "chunkIndex", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AIConversationEmbedding_sourceId_chunkIndex_key", + "entityType": "indexes", + "schema": "public", + "table": "AIConversationEmbedding" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "embedding", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "vector_cosine_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "hnsw", + "concurrently": false, + "name": "AIConversationEmbedding_embedding_idx", + "entityType": "indexes", + "schema": "public", + "table": "AIConversationEmbedding" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "conversationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AIConversationEmbedding_conversationId_idx", + "entityType": "indexes", + "schema": "public", + "table": "AIConversationEmbedding" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "conversationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "sourceType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AIConversationSource_lookup_idx", + "entityType": "indexes", + "schema": "public", + "table": "AIConversationSource" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "sourceType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "sourceKey", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AIConversationSource_workspaceId_sourceType_sourceKey_key", + "entityType": "indexes", + "schema": "public", + "table": "AIConversationSource" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "messageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AIConversationSource_messageId_idx", + "entityType": "indexes", + "schema": "public", + "table": "AIConversationSource" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "conversationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AIConversationSource_conversationId_idx", + "entityType": "indexes", + "schema": "public", + "table": "AIConversationSource" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AIEmbedding_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "AIEmbedding" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "occurredAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AnalyticsBotMessageEvent_workspaceId_occurredAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "AnalyticsBotMessageEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "aiProvider", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "occurredAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AnalyticsBotMessageEvent_workspaceId_aiProvider_occurredAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "AnalyticsBotMessageEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "hasResponse", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "result", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "occurredAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AnalyticsBotMessageEvent_workspaceId_hasResponse_result_occurredAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "AnalyticsBotMessageEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "broadcastId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "eventType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "occurredAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AnalyticsBroadcastEvent_workspaceId_broadcastId_eventType_occurredAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "AnalyticsBroadcastEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "occurredAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "eventType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AnalyticsContactEvent_workspaceId_occurredAt_eventType_idx", + "entityType": "indexes", + "schema": "public", + "table": "AnalyticsContactEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "eventType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "occurredAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AnalyticsContactEvent_workspaceId_eventType_occurredAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "AnalyticsContactEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "adminId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "occurredAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AnalyticsContactEvent_workspaceId_adminId_occurredAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "AnalyticsContactEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "occurredAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "eventType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AnalyticsConversationEvent_workspaceId_occurredAt_eventType_idx", + "entityType": "indexes", + "schema": "public", + "table": "AnalyticsConversationEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "toAssignee", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "occurredAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AnalyticsConversationEvent_workspaceId_toAssignee_occurredAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "AnalyticsConversationEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "flowId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "analyticsId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "nodeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "occurredAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AnalyticsFlowNodeEvent_workspaceId_flowId_analyticsId_nodeId_occurredAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "AnalyticsFlowNodeEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "flowId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "analyticsId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "nodeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "buttonId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "occurredAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AnalyticsFlowNodeEvent_workspaceId_flowId_analyticsId_nodeId_buttonId_occurredAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "AnalyticsFlowNodeEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "occurredAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "eventType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AnalyticsMessageEvent_workspaceId_occurredAt_eventType_idx", + "entityType": "indexes", + "schema": "public", + "table": "AnalyticsMessageEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "eventType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "occurredAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AnalyticsMessageEvent_workspaceId_eventType_occurredAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "AnalyticsMessageEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "adminId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "occurredAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AnalyticsMessageEvent_workspaceId_adminId_occurredAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "AnalyticsMessageEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "senderType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "occurredAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AnalyticsMessageEvent_workspaceId_senderType_occurredAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "AnalyticsMessageEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "sequenceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "stepId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "eventType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "occurredAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AnalyticsSequenceEvent_workspaceId_sequenceId_stepId_eventType_occurredAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "AnalyticsSequenceEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "token", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AnalyticsEmailTopic_token_key", + "entityType": "indexes", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "topicId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AnalyticsEmailTopic_topicId_idx", + "entityType": "indexes", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "topicId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AnalyticsEmailTopic_workspaceId_topicId_idx", + "entityType": "indexes", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "objectKey", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AnalyticsManifestStatus_objectKey_key", + "entityType": "indexes", + "schema": "public", + "table": "AnalyticsManifestStatus" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Appointment_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Appointment" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "startAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Appointment_workspaceId_startAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "Appointment" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "calendarId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "startAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Appointment_calendarId_status_startAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "Appointment" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "calendarId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Appointment_contactId_calendarId_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "Appointment" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AppointmentCalendar_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "(\"deletedAt\" is null)", + "with": "", + "method": "btree", + "concurrently": false, + "name": "AppointmentCalendar_workspaceId_name_key", + "entityType": "indexes", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "publicLinkSlug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AppointmentCalendar_publicLinkSlug_key", + "entityType": "indexes", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "calendarId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AppointmentCalendarAvailability_calendarId_idx", + "entityType": "indexes", + "schema": "public", + "table": "AppointmentCalendarAvailability" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "calendarId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "flowId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "timingValue", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "timingUnit", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AppointmentCalendarReminder_dedupe_key", + "entityType": "indexes", + "schema": "public", + "table": "AppointmentCalendarReminder" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "jobId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AppointmentReminderDispatch_jobId_key", + "entityType": "indexes", + "schema": "public", + "table": "AppointmentReminderDispatch" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "sendAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AppointmentReminderDispatch_status_sendAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "AppointmentReminderDispatch" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "appointmentId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AppointmentReminderDispatch_appointmentId_idx", + "entityType": "indexes", + "schema": "public", + "table": "AppointmentReminderDispatch" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactInboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AppointmentReminderDispatch_contactInboxId_idx", + "entityType": "indexes", + "schema": "public", + "table": "AppointmentReminderDispatch" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "messageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "messageCreatedAt", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Attachment_message_idx", + "entityType": "indexes", + "schema": "public", + "table": "Attachment" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Attachment_workspaceId_createdAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "Attachment" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "conversationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Attachment_conversationId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Attachment" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "providerId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "accountId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "tenantId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Account_providerId_accountId_tenantId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Account" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "code", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Invitation_code_key", + "entityType": "indexes", + "schema": "public", + "table": "Invitation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "token", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Session_token_key", + "entityType": "indexes", + "schema": "public", + "table": "Session" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "email", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "tenantId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "User_email_tenant_key", + "entityType": "indexes", + "schema": "public", + "table": "User" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tenantId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "User_tenantId_idx", + "entityType": "indexes", + "schema": "public", + "table": "User" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AutomatedResponse_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "AutomatedResponse" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "lastTriggeredAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AutomationThrottle_lastTriggeredAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "AutomationThrottle" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "BotField_workspaceId_type_name_key", + "entityType": "indexes", + "schema": "public", + "table": "BotField" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Broadcast_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Broadcast" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "flowId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Broadcast_flowId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Broadcast" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "channel", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Broadcast_channel_idx", + "entityType": "indexes", + "schema": "public", + "table": "Broadcast" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "schedulesAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Broadcast_schedulesAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "Broadcast" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Broadcast_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "Broadcast" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "deletedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"deletedAt\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "Broadcast_deletedAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "Broadcast" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "inboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "BroadcastTarget_inboxId_idx", + "entityType": "indexes", + "schema": "public", + "table": "BroadcastTarget" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "flowId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "BroadcastTarget_flowId_idx", + "entityType": "indexes", + "schema": "public", + "table": "BroadcastTarget" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "CoexistSyncRun_workspace_idx", + "entityType": "indexes", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "CoexistSyncRun_integration_idx", + "entityType": "indexes", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "lastHeartbeatAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "status IN ('init', 'running')", + "with": "", + "method": "btree", + "concurrently": false, + "name": "CoexistSyncRun_active_idx", + "entityType": "indexes", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "channel", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "\"startedAt\" DESC", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "status IN ('succeeded', 'partial')", + "with": "", + "method": "btree", + "concurrently": false, + "name": "CoexistSyncRun_integration_resume_idx", + "entityType": "indexes", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "channel", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "status = 'init' AND type = 'coexist'", + "with": "", + "method": "btree", + "concurrently": false, + "name": "CoexistSyncRun_integration_init_uq", + "entityType": "indexes", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "type = 'contact_scan' AND status IN ('init', 'running')", + "with": "", + "method": "btree", + "concurrently": false, + "name": "CoexistSyncRun_contact_scan_active_uq", + "entityType": "indexes", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "type = 'contact_scan'", + "with": "", + "method": "btree", + "concurrently": false, + "name": "CoexistSyncRun_contact_scan_due_idx", + "entityType": "indexes", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "broadcastSubscribedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_contact_broadcast_subscribed_at", + "entityType": "indexes", + "schema": "public", + "table": "Contact" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_contact_workspace_created_at", + "entityType": "indexes", + "schema": "public", + "table": "Contact" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "email", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_contact_workspace_email", + "entityType": "indexes", + "schema": "public", + "table": "Contact" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "phoneNumber", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_contact_workspace_phone_number", + "entityType": "indexes", + "schema": "public", + "table": "Contact" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "firstName", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "gin_trgm_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "Contact_firstName_trgm_idx", + "entityType": "indexes", + "schema": "public", + "table": "Contact" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "lastName", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "gin_trgm_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "Contact_lastName_trgm_idx", + "entityType": "indexes", + "schema": "public", + "table": "Contact" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "email", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "gin_trgm_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "Contact_email_trgm_idx", + "entityType": "indexes", + "schema": "public", + "table": "Contact" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "phoneNumber", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "gin_trgm_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "Contact_phoneNumber_trgm_idx", + "entityType": "indexes", + "schema": "public", + "table": "Contact" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "customFieldId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ContactCustomField_contactId_customFieldId_key", + "entityType": "indexes", + "schema": "public", + "table": "ContactCustomField" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "customFieldId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ContactCustomField_customFieldId_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "ContactCustomField" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "inboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "sourceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ContactInbox_inboxId_sourceId_key", + "entityType": "indexes", + "schema": "public", + "table": "ContactInbox" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "inboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "sourceUserId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"sourceUserId\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "ContactInbox_inboxId_sourceUserId_key", + "entityType": "indexes", + "schema": "public", + "table": "ContactInbox" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "lastIncomingMessageAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ContactInbox_contactId_lastIncomingMessageAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "ContactInbox" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "lastOutboundMessageAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ContactInbox_contactId_lastOutboundMessageAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "ContactInbox" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "(\"referral\"->>'ctwaClid')", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"referral\"->>'ctwaClid' IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "ContactInbox_referral_ctwaClid_idx", + "entityType": "indexes", + "schema": "public", + "table": "ContactInbox" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "(\"referral\"->>'adId')", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"referral\"->>'adId' IS NOT NULL AND \"referral\"->>'source' = 'ADS'", + "with": "", + "method": "btree", + "concurrently": false, + "name": "ContactInbox_referral_adId_idx", + "entityType": "indexes", + "schema": "public", + "table": "ContactInbox" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ContactNote_contactId_idx", + "entityType": "indexes", + "schema": "public", + "table": "ContactNote" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_contact_on_broadcast_contact_id", + "entityType": "indexes", + "schema": "public", + "table": "ContactOnBroadcast" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "isRead", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_contact_on_broadcast_is_read", + "entityType": "indexes", + "schema": "public", + "table": "ContactOnBroadcast" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "broadcastId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"sent\" = false AND \"failedAt\" IS NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "ContactOnBroadcast_unsent_idx", + "entityType": "indexes", + "schema": "public", + "table": "ContactOnBroadcast" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "sequenceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ContactsOnSequence_sequenceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ContactsOnSequence_contactId_idx", + "entityType": "indexes", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ContactsOnSequence_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "nextRunAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ContactsOnSequence_status_nextRunAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "nextRunAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ContactsOnSequence_workspaceId_status_nextRunAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "sequenceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ContactsOnSequence_contactId_sequenceId_workspaceId_key", + "entityType": "indexes", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "flowId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "contactInboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "stepId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ContactOnSmartDelay_workspaceId_flowId_contactInboxId_stepId_idx", + "entityType": "indexes", + "schema": "public", + "table": "ContactOnSmartDelay" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "triggerAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ContactOnSmartDelay_status_triggerAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "ContactOnSmartDelay" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "conversationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ContactOnSmartDelay_conversationId_idx", + "entityType": "indexes", + "schema": "public", + "table": "ContactOnSmartDelay" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "appointmentId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ContactOnSmartDelay_appointmentId_idx", + "entityType": "indexes", + "schema": "public", + "table": "ContactOnSmartDelay" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "contactInboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "flowId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "stepId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"status\" NOT IN ('completed', 'failed', 'canceled') AND \"type\" = 'followUp'", + "with": "", + "method": "btree", + "concurrently": false, + "name": "ContactOnSmartDelay_followUp_active_key", + "entityType": "indexes", + "schema": "public", + "table": "ContactOnSmartDelay" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactInboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ContactToTagChannel_contactInboxId_idx", + "entityType": "indexes", + "schema": "public", + "table": "ContactToTagChannel" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tagId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ContactToTagChannel_tagId_idx", + "entityType": "indexes", + "schema": "public", + "table": "ContactToTagChannel" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "sourceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"sourceId\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "Conversation_contactId_sourceId_key", + "entityType": "indexes", + "schema": "public", + "table": "Conversation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"sourceId\" IS NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "Conversation_contactId_dm_key", + "entityType": "indexes", + "schema": "public", + "table": "Conversation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "aiContextLastMessageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Conversation_aiContextLastMessageId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Conversation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "lastActivityAt", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Conversation_workspaceId_lastActivityAt_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "Conversation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ConversationParticipant_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "ConversationParticipant" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "conversationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ConversationParticipant_conversationId_idx", + "entityType": "indexes", + "schema": "public", + "table": "ConversationParticipant" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "conversationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ConversationParticipant_conversationId_userId_key", + "entityType": "indexes", + "schema": "public", + "table": "ConversationParticipant" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "code", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Coupon_workspaceId_code_key", + "entityType": "indexes", + "schema": "public", + "table": "Coupon" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "topicId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "issuedContactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"issuedContactId\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "Coupon_workspaceId_topicId_issuedContactId_key", + "entityType": "indexes", + "schema": "public", + "table": "Coupon" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "topicId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Coupon_workspaceId_topicId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Coupon" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "topicId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "issuedContactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Coupon_workspaceId_topicId_issuedContactId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Coupon" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "issuedContactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Coupon_issuedContactId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Coupon" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "issuedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Coupon_issuedAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "Coupon" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "usedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Coupon_usedAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "Coupon" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "code", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Coupon_workspaceId_code_idx", + "entityType": "indexes", + "schema": "public", + "table": "Coupon" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "topicId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "issuedContactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "usedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Coupon_workspaceId_topicId_issuedContactId_usedAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "Coupon" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "deletedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "CouponTopic_workspaceId_status_deletedAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "CouponTopic" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "expiresAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "CouponTopic_workspaceId_expiresAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "CouponTopic" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "lower(\"name\")", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"deletedAt\" IS NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "CouponTopic_workspaceId_name_idx", + "entityType": "indexes", + "schema": "public", + "table": "CouponTopic" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "CustomField_workspaceId_type_name_key", + "entityType": "indexes", + "schema": "public", + "table": "CustomField" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "DynamicImage_workspaceId_name_key", + "entityType": "indexes", + "schema": "public", + "table": "DynamicImage" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "EmailTopic_workspaceId_name_key", + "entityType": "indexes", + "schema": "public", + "table": "EmailTopic" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "folderId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "EmailTopic_folderId_idx", + "entityType": "indexes", + "schema": "public", + "table": "EmailTopic" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AuditLog_workspaceId_createdAt_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "AuditLog" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"userId\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "AuditLog_workspaceId_userId_createdAt_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "AuditLog" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tenantId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "CustomDomain_tenantId_key", + "entityType": "indexes", + "schema": "public", + "table": "CustomDomain" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "domain", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "CustomDomain_domain_key", + "entityType": "indexes", + "schema": "public", + "table": "CustomDomain" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "CustomDomain_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "CustomDomain" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "ownerId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Tenant_ownerId_key", + "entityType": "indexes", + "schema": "public", + "table": "Tenant" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tenantId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "TenantHelpItem_tenantId_position_idx", + "entityType": "indexes", + "schema": "public", + "table": "TenantHelpItem" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "channelsTornDownAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "UserQuota_channelsTornDownAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "UserQuota" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"channelsTornDownAt\" IS NULL AND \"planStatus\" = 'trial'", + "with": "", + "method": "btree", + "concurrently": false, + "name": "UserQuota_due_expired_trial_idx", + "entityType": "indexes", + "schema": "public", + "table": "UserQuota" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": false, + "nullsFirst": true, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ErrorLog_workspaceId_createdAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "ErrorLog" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ErrorLog_createdAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "ErrorLog" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ErrorLog_contactId_idx", + "entityType": "indexes", + "schema": "public", + "table": "ErrorLog" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "event", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ExternalWebhook_workspaceId_event_idx", + "entityType": "indexes", + "schema": "public", + "table": "ExternalWebhook" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "event", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "url", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ExternalWebhook_workspaceId_event_url_key", + "entityType": "indexes", + "schema": "public", + "table": "ExternalWebhook" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "pageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "formId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "FacebookLeadAdsAutomation_workspaceId_pageId_formId_key", + "entityType": "indexes", + "schema": "public", + "table": "FacebookLeadAdsAutomation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "automationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "leadgenId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "FacebookLeadAdsLead_automationId_leadgenId_key", + "entityType": "indexes", + "schema": "public", + "table": "FacebookLeadAdsLead" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "leadgenId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "FacebookLeadAdsLead_leadgenId_idx", + "entityType": "indexes", + "schema": "public", + "table": "FacebookLeadAdsLead" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "FBCommentAutomation_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "folderId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "FBCommentAutomation_folderId_idx", + "entityType": "indexes", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "automationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "commentId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "replyChannel", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "FBCommentAutomationEvent_dedup_idx", + "entityType": "indexes", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "automationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "occurredAt", + "isExpression": false, + "asc": false, + "nullsFirst": true, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "FBCommentAutomationEvent_automation_occurredAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "FBCommentAutomationEvent_contactId_idx", + "entityType": "indexes", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "automationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "postId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "FBCommentAutomationReply_dedup_idx", + "entityType": "indexes", + "schema": "public", + "table": "FBCommentAutomationReply" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "FBCommentAutomationReply_contactId_idx", + "entityType": "indexes", + "schema": "public", + "table": "FBCommentAutomationReply" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "path", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "File_path_key", + "entityType": "indexes", + "schema": "public", + "table": "File" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "contextType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "File_workspaceId_contextType_idx", + "entityType": "indexes", + "schema": "public", + "table": "File" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "subType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "File_workspaceId_subType_idx", + "entityType": "indexes", + "schema": "public", + "table": "File" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "flowId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"deletedAt\" IS NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "FlowAnalyticsSession_workspaceId_flowId_key", + "entityType": "indexes", + "schema": "public", + "table": "FlowAnalyticsSession" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "flowId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "FlowAnalyticsSession_flowId_idx", + "entityType": "indexes", + "schema": "public", + "table": "FlowAnalyticsSession" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "analyticsId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "nodeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "eventType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "buttonId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "FlowNodeStat_filter_1_idx", + "entityType": "indexes", + "schema": "public", + "table": "FlowNodeStat" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "analyticsId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "nodeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"eventType\" = 'seen' AND \"seenAt\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "FlowNodeStat_filter_2_idx", + "entityType": "indexes", + "schema": "public", + "table": "FlowNodeStat" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "conversationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "FlowRun_conversationId_idx", + "entityType": "indexes", + "schema": "public", + "table": "FlowRun" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Folder_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Folder" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "parentId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Folder_parentId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Folder" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IgStoryAutomation_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "IgStoryAutomation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "folderId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IgStoryAutomation_folderId_idx", + "entityType": "indexes", + "schema": "public", + "table": "IgStoryAutomation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Import_workspaceId_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "Import" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Import_workspaceId_type_idx", + "entityType": "indexes", + "schema": "public", + "table": "Import" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "inboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Import_inboxId_type_idx", + "entityType": "indexes", + "schema": "public", + "table": "Import" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "fileId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Import_fileId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Import" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"type\" = 'products' AND \"status\" IN ('pending', 'processing')", + "with": "", + "method": "btree", + "concurrently": false, + "name": "Import_products_active_idx", + "entityType": "indexes", + "schema": "public", + "table": "Import" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Inbox_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Inbox" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "channel", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "sourceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Inbox_workspaceId_channel_sourceId_key", + "entityType": "indexes", + "schema": "public", + "table": "Inbox" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationActiveCampaign_integrationId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationActiveCampaign" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationActiveCampaign_workspaceId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationActiveCampaign" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationApi_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationApi" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "inboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationApi_inboxId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationApi" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tokenHash", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationApi_tokenHash_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationApi" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Integration_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Integration" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "integrationType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Integration_workspaceId_integrationType_key", + "entityType": "indexes", + "schema": "public", + "table": "Integration" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationClaude_workspaceId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationClaude" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationClaude_integrationId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationClaude" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationDeepseek_workspaceId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationDeepseek" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationDeepseek_integrationId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationDeepseek" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationDrip_integrationId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationDrip" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationDrip_workspaceId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationDrip" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationFacebookAds_integrationId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationFacebookAds" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationFacebookAds_workspaceId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationFacebookAds" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationGemini_workspaceId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationGemini" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationGemini_integrationId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationGemini" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationGetResponse_integrationId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationGetResponse" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationGetResponse_workspaceId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationGetResponse" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationGoogleCalendar_integrationId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationGoogleCalendar" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationGoogleSheet_integrationId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationGoogleSheet" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationInstagram_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "welcomeFlowId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationInstagram_welcomeFlowId_idx", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "inboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationInstagram_inboxId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "igId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationInstagram_igId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationKlaviyo_workspaceId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationKlaviyo" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationKlaviyo_integrationId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationKlaviyo" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationMailchimp_integrationId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationMailchimp" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationMailchimp_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationMailchimp" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationMailerLite_workspaceId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationMailerLite" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationMailerLite_integrationId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationMailerLite" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationMessenger_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "welcomeFlowId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationMessenger_welcomeFlowId_idx", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "inboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationMessenger_inboxId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "pageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationMessenger_pageId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationMetaCatalog_integrationId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationMetaCatalog_workspaceId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "deletedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationMetaCatalog_deletedAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationMoosend_workspaceId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationMoosend" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationMoosend_integrationId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationMoosend" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationOpenAI_integrationId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationOpenai" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationOpenaiCompatible_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationOpenaiCompatible" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationOpenaiCompatible_integrationId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationOpenaiCompatible" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "preset", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"preset\" <> 'custom'", + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationOpenaiCompatible_workspaceId_preset_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationOpenaiCompatible" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationOpenrouter_workspaceId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationOpenrouter" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationOpenrouter_integrationId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationOpenrouter" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationOutlookCalendar_integrationId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationOutlookCalendar" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationSendGrid_integrationId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationSendGrid" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationSendGrid_workspaceId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationSendGrid" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationSmtp_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationSmtp" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "inboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationSmtp_inboxId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationSmtp" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationTelegram_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationTelegram" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "inboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationTelegram_inboxId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationTelegram" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "botId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationTelegram_botId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationTelegram" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationTiktok_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationTiktok" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "inboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationTiktok_inboxId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationTiktok" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "openId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationTiktok_openId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationTiktok" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationWebchat_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationWebchat" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "inboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationWebchat_inboxId_idx", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationWebchat" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "inboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationWebchat_inboxId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationWebchat" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "welcomeFlowId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationWebchat_welcomeFlowId_idx", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationWebchat" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "inboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationWhatsapp_inboxId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationWhatsapp_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "phoneNumberId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationWhatsapp_phoneNumberId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationZalo_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationZalo" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "fallbackFlowId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationZalo_fallbackFlowId_idx", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationZalo" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "inboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationZalo_inboxId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationZalo" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MagicLink_workspaceId_name_key", + "entityType": "indexes", + "schema": "public", + "table": "MagicLink" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MagicLink_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "MagicLink" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "linkId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "occurredAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "contactInboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MagicLinkStat_workspaceId_linkId_occurredAt_contactInboxId_key", + "entityType": "indexes", + "schema": "public", + "table": "MagicLinkStat" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MediaLibraryFile_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "MediaLibraryFile" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "folderId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MediaLibraryFile_folderId_idx", + "entityType": "indexes", + "schema": "public", + "table": "MediaLibraryFile" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MediaLibraryFolder_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "MediaLibraryFolder" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "conversationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Message_conversation_history_idx", + "entityType": "indexes", + "schema": "public", + "table": "Message" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Message_workspace_created_idx", + "entityType": "indexes", + "schema": "public", + "table": "Message" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactInboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "sourceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Message_contactInboxId_sourceId_createdAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "Message" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "conversationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Message_conversationId_type_idx", + "entityType": "indexes", + "schema": "public", + "table": "Message" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "parentId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"parentId\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "Message_parentId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Message" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "inboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "sourceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MessageCleanup_inboxId_sourceId_key", + "entityType": "indexes", + "schema": "public", + "table": "MessageCleanup" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MessageCleanup_status_createdAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "MessageCleanup" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MessageCleanup_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "MessageCleanup" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MessagingAdOperation_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationWhatsappId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MessagingAdOperation_integrationWhatsappId_idx", + "entityType": "indexes", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationMessengerId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MessagingAdOperation_integrationMessengerId_idx", + "entityType": "indexes", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationInstagramId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MessagingAdOperation_integrationInstagramId_idx", + "entityType": "indexes", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MessagingAdsConnection_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "MessagingAdsConnection" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationWhatsappId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MessagingAdsConnection_integrationWhatsappId_key", + "entityType": "indexes", + "schema": "public", + "table": "MessagingAdsConnection" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationMessengerId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MessagingAdsConnection_integrationMessengerId_key", + "entityType": "indexes", + "schema": "public", + "table": "MessagingAdsConnection" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationInstagramId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MessagingAdsConnection_integrationInstagramId_key", + "entityType": "indexes", + "schema": "public", + "table": "MessagingAdsConnection" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationMessengerId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "sourceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MessengerMessageTemplate_integrationMessengerId_sourceId_key", + "entityType": "indexes", + "schema": "public", + "table": "MessengerMessageTemplate" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "channel", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "sourceKey", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MetaCapiEvent_workspaceId_channel_sourceKey_key", + "entityType": "indexes", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactInboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MetaCapiEvent_contactInboxId_idx", + "entityType": "indexes", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "channel", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MetaCapiEvent_channel_integrationId_idx", + "entityType": "indexes", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationMetaCatalogId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "catalogId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "retailerId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MetaCatalogItem_integration_retailer_key", + "entityType": "indexes", + "schema": "public", + "table": "MetaCatalogItem" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationMetaCatalogId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "catalogId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "productId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MetaCatalogItem_integration_product_key", + "entityType": "indexes", + "schema": "public", + "table": "MetaCatalogItem" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "productId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MetaCatalogItem_productId_idx", + "entityType": "indexes", + "schema": "public", + "table": "MetaCatalogItem" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"status\" IN ('queued', 'running')", + "with": "", + "method": "btree", + "concurrently": false, + "name": "MetaCatalogSyncRun_active_idx", + "entityType": "indexes", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Minigame_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Minigame" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Minigame_workspaceId_name_key", + "entityType": "indexes", + "schema": "public", + "table": "Minigame" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "minigameId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MinigameContact_minigameId_idx", + "entityType": "indexes", + "schema": "public", + "table": "MinigameContact" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MinigameContact_contactId_idx", + "entityType": "indexes", + "schema": "public", + "table": "MinigameContact" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "minigameId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MinigameContact_minigameId_contactId_key", + "entityType": "indexes", + "schema": "public", + "table": "MinigameContact" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "minigameId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MinigamePlay_minigameId_contactId_idx", + "entityType": "indexes", + "schema": "public", + "table": "MinigamePlay" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MinigamePlay_contactId_idx", + "entityType": "indexes", + "schema": "public", + "table": "MinigamePlay" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "livemode", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"userId\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "PlatformCredential_user_type_livemode_key", + "entityType": "indexes", + "schema": "public", + "table": "PlatformCredential" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "livemode", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"userId\" IS NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "PlatformCredential_platform_type_livemode_key", + "entityType": "indexes", + "schema": "public", + "table": "PlatformCredential" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "PlatformCredential_userId_idx", + "entityType": "indexes", + "schema": "public", + "table": "PlatformCredential" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Product_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Product" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "categoryId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Product_categoryId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Product" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "subcategoryId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Product_subcategoryId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Product" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "productId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ProductAddon_productId_idx", + "entityType": "indexes", + "schema": "public", + "table": "ProductAddon" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ProductCategory_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "ProductCategory" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "parentId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ProductCategory_parentId_idx", + "entityType": "indexes", + "schema": "public", + "table": "ProductCategory" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "productId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ProductVariant_productId_idx", + "entityType": "indexes", + "schema": "public", + "table": "ProductVariant" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "productId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ProductVariantOption_productId_idx", + "entityType": "indexes", + "schema": "public", + "table": "ProductVariantOption" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "submissionId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "questionIdSnapshot", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "QuestionnaireAnswer_submissionId_questionIdSnapshot_key", + "entityType": "indexes", + "schema": "public", + "table": "QuestionnaireAnswer" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "questionId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "QuestionnaireAnswer_questionId_idx", + "entityType": "indexes", + "schema": "public", + "table": "QuestionnaireAnswer" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Questionnaire_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Questionnaire" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "(\"deletedAt\" is null)", + "with": "", + "method": "btree", + "concurrently": false, + "name": "Questionnaire_workspaceId_name_key", + "entityType": "indexes", + "schema": "public", + "table": "Questionnaire" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "questionnaireId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "orderNo", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "QuestionnaireQuestion_questionnaireId_orderNo_idx", + "entityType": "indexes", + "schema": "public", + "table": "QuestionnaireQuestion" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "customFieldId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "QuestionnaireQuestion_customFieldId_idx", + "entityType": "indexes", + "schema": "public", + "table": "QuestionnaireQuestion" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "QuestionnaireSubmission_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "QuestionnaireSubmission" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "questionnaireId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "QuestionnaireSubmission_questionnaireId_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "QuestionnaireSubmission" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "questionnaireId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "QuestionnaireSubmission_questionnaireId_contactId_idx", + "entityType": "indexes", + "schema": "public", + "table": "QuestionnaireSubmission" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"status\" = 'inProgress'", + "with": "", + "method": "btree", + "concurrently": false, + "name": "QuestionnaireSubmission_workspaceId_contactId_active_key", + "entityType": "indexes", + "schema": "public", + "table": "QuestionnaireSubmission" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "RefLinkStat_contactId_idx", + "entityType": "indexes", + "schema": "public", + "table": "RefLinkStat" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "linkId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "occurredAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "contactInboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "RefLinkStat_workspaceId_linkId_occurredAt_contactInboxId_key", + "entityType": "indexes", + "schema": "public", + "table": "RefLinkStat" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Reflink_workspaceId_name_key", + "entityType": "indexes", + "schema": "public", + "table": "Reflink" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "folderId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Sequence_folderId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Sequence" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Sequence_workspaceId_name_key", + "entityType": "indexes", + "schema": "public", + "table": "Sequence" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "idempotencyKey", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "SequenceDispatch_idempotencyKey_key", + "entityType": "indexes", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "SequenceDispatch_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "runAtMs", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"status\" = 'pending'", + "with": "", + "method": "btree", + "concurrently": false, + "name": "SequenceDispatch_pending_runAtMs_idx", + "entityType": "indexes", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "bucket", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "runAtMs", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"status\" = 'pending'", + "with": "", + "method": "btree", + "concurrently": false, + "name": "SequenceDispatch_pending_bucket_runAtMs_idx", + "entityType": "indexes", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "runAtMs", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "SequenceDispatch_status_runAtMs_idx", + "entityType": "indexes", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "runAtMs", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "SequenceDispatch_workspaceId_status_runAtMs_idx", + "entityType": "indexes", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "enrollmentId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "SequenceDispatch_enrollmentId_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "sequenceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "stepId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "SequenceDispatch_workspace_sequence_step_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "bucket", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "runAtMs", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "SequenceDispatch_bucket_status_runAtMs_idx", + "entityType": "indexes", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"status\" in ('completed', 'failed', 'canceled')", + "with": "", + "method": "btree", + "concurrently": false, + "name": "SequenceDispatch_terminal_updatedAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "SequenceDispatch_contactId_idx", + "entityType": "indexes", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "sequenceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "SequenceStep_sequenceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "SequenceStep" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "flowId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "SequenceStep_flowId_idx", + "entityType": "indexes", + "schema": "public", + "table": "SequenceStep" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Spreadsheet_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Spreadsheet" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "spreadsheetId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Spreadsheet_workspaceId_spreadsheetId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Spreadsheet" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "spreadsheetId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Spreadsheet_spreadsheetId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Spreadsheet" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "(\"deletedAt\" is null)", + "with": "", + "method": "btree", + "concurrently": false, + "name": "Tag_workspaceId_name_key", + "entityType": "indexes", + "schema": "public", + "table": "Tag" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "folderId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Tag_folderId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Tag" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tagId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "channelType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "TagChannel_tag_integration_key", + "entityType": "indexes", + "schema": "public", + "table": "TagChannel" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "channelType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "externalLabelId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "TagChannel_external_key", + "entityType": "indexes", + "schema": "public", + "table": "TagChannel" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "channelType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "TagChannel_workspace_channel_idx", + "entityType": "indexes", + "schema": "public", + "table": "TagChannel" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "channelType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "TagChannel_integration_idx", + "entityType": "indexes", + "schema": "public", + "table": "TagChannel" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Template_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Template" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tenantId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Template_tenantId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Template" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "shareToken", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Template_shareToken_key", + "entityType": "indexes", + "schema": "public", + "table": "Template" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "TemplateInstallation_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "templateId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "TemplateInstallation_templateId_idx", + "entityType": "indexes", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "TemplateInstallation_workspaceId_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "installationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "TemplateInstalledResource_installationId_idx", + "entityType": "indexes", + "schema": "public", + "table": "TemplateInstalledResource" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "resourceKind", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "resourceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "TemplateInstalledResource_workspaceId_resourceKind_resourceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "TemplateInstalledResource" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Trigger_workspaceId_name_key", + "entityType": "indexes", + "schema": "public", + "table": "Trigger" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Trigger_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Trigger" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "folderId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Trigger_folderId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Trigger" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "active", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Trigger_workspaceId_active_idx", + "entityType": "indexes", + "schema": "public", + "table": "Trigger" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "sourceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Condition_type_source_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "Condition" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "triggerId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Condition_triggerId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Condition" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "webhookId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Condition_webhookId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Condition" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "sourceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "triggerId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Condition_type_sourceId_triggerId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Condition" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "sourceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "webhookId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Condition_type_sourceId_webhookId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Condition" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "triggerId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "TriggerContactHistory_triggerId_contactId_idx", + "entityType": "indexes", + "schema": "public", + "table": "TriggerContactHistory" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "TriggerContactHistory_contactId_idx", + "entityType": "indexes", + "schema": "public", + "table": "TriggerContactHistory" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "TriggerContactHistory_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "TriggerContactHistory" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "triggerId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "TriggerExecution_triggerId_contactId_idx", + "entityType": "indexes", + "schema": "public", + "table": "TriggerExecution" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "TriggerExecution_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "TriggerExecution" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "TriggerExecution_contactId_idx", + "entityType": "indexes", + "schema": "public", + "table": "TriggerExecution" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "triggerId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "date", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "TriggerStat_triggerId_date_key", + "entityType": "indexes", + "schema": "public", + "table": "TriggerStat" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "triggerId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "date", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "TriggerStat_triggerId_date_idx", + "entityType": "indexes", + "schema": "public", + "table": "TriggerStat" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "date", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "TriggerStat_workspaceId_date_idx", + "entityType": "indexes", + "schema": "public", + "table": "TriggerStat" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "UserDeviceToken_userId_idx", + "entityType": "indexes", + "schema": "public", + "table": "UserDeviceToken" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "UserPersistentMenu_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "UserPersistentMenu" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Webhook_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Webhook" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "folderId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Webhook_folderId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Webhook" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "active", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Webhook_workspaceId_active_idx", + "entityType": "indexes", + "schema": "public", + "table": "Webhook" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "webhookId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "WebhookExecution_webhookId_contactId_key", + "entityType": "indexes", + "schema": "public", + "table": "WebhookExecution" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "WebhookExecution_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "WebhookExecution" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "WebhookExecution_contactId_idx", + "entityType": "indexes", + "schema": "public", + "table": "WebhookExecution" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "wabaId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "WhatsappBusinessAccount_workspaceId_wabaId_key", + "entityType": "indexes", + "schema": "public", + "table": "WhatsappBusinessAccount" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "phoneNumberId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "WhatsappCoexistStaging_phoneNumberId_idx", + "entityType": "indexes", + "schema": "public", + "table": "WhatsappCoexistStaging" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "phoneNumberId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "payloadHash", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "WhatsappCoexistStaging_phone_hash_uq", + "entityType": "indexes", + "schema": "public", + "table": "WhatsappCoexistStaging" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "processedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"processedAt\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "WhatsappCoexistStaging_processedAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "WhatsappCoexistStaging" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "parseFailedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"parseFailedAt\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "WhatsappCoexistStaging_parseFailedAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "WhatsappCoexistStaging" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationWhatsappId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "sourceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "WhatsappFlow_integrationWhatsappId_sourceId_key", + "entityType": "indexes", + "schema": "public", + "table": "WhatsappFlow" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationWhatsappId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "sourceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "WhatsappMessageTemplate_integrationWhatsappId_sourceId_key", + "entityType": "indexes", + "schema": "public", + "table": "WhatsappMessageTemplate" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "WhatsappSignupSession_userId_idx", + "entityType": "indexes", + "schema": "public", + "table": "WhatsappSignupSession" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "expiresAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "WhatsappSignupSession_expiresAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "WhatsappSignupSession" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tenantId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Workspace_tenantId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Workspace" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "scheduledDeletionAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Workspace_scheduledDeletionAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "Workspace" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "supportAccessUntil", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Workspace_supportAccessUntil_idx", + "entityType": "indexes", + "schema": "public", + "table": "Workspace" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "WorkspaceApiToken_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "WorkspaceApiToken" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"isDefault\"", + "with": "", + "method": "btree", + "concurrently": false, + "name": "WorkspaceApiToken_workspaceId_default_key", + "entityType": "indexes", + "schema": "public", + "table": "WorkspaceApiToken" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactInboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "FBCommentAutomationEvent_contactInboxId_idx", + "entityType": "indexes", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactInboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"replyChannel\" = 'private' AND \"deliveredAt\" IS NOT NULL AND \"seenAt\" IS NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "FBCommentAutomationEvent_private_unseen_idx", + "entityType": "indexes", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"status\" = 'failed'", + "with": "", + "method": "btree", + "concurrently": false, + "name": "FBCommentAutomationEvent_failed_createdAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "automationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "commentId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "FBCommentAutomationMiss_dedup_idx", + "entityType": "indexes", + "schema": "public", + "table": "FBCommentAutomationMiss" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "automationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "occurredAt", + "isExpression": false, + "asc": false, + "nullsFirst": true, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "FBCommentAutomationMiss_automation_occurredAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "FBCommentAutomationMiss" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "FBCommentAutomationMiss_contactId_idx", + "entityType": "indexes", + "schema": "public", + "table": "FBCommentAutomationMiss" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactInboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "FBCommentAutomationMiss_contactInboxId_idx", + "entityType": "indexes", + "schema": "public", + "table": "FBCommentAutomationMiss" + }, + { + "nameExplicit": false, + "columns": [ + "shardId" + ], + "schemaTo": "public", + "tableTo": "MessageShard", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ShardTimeRange_shardId_MessageShard_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ShardTimeRange" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AdsConversionEvent_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "nameExplicit": false, + "columns": [ + "integrationWhatsappId" + ], + "schemaTo": "public", + "tableTo": "IntegrationWhatsapp", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AdsConversionEvent_IENNsXLBb1k1_fkey", + "entityType": "fks", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "nameExplicit": false, + "columns": [ + "integrationMessengerId" + ], + "schemaTo": "public", + "tableTo": "IntegrationMessenger", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AdsConversionEvent_yeTDEtvJMs4H_fkey", + "entityType": "fks", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "nameExplicit": false, + "columns": [ + "integrationInstagramId" + ], + "schemaTo": "public", + "tableTo": "IntegrationInstagram", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AdsConversionEvent_1X1qUqZpm24x_fkey", + "entityType": "fks", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "nameExplicit": false, + "columns": [ + "contactInboxId" + ], + "schemaTo": "public", + "tableTo": "ContactInbox", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "AdsConversionEvent_contactInboxId_ContactInbox_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AdsConversionRule_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AdsConversionRule" + }, + { + "nameExplicit": false, + "columns": [ + "integrationWhatsappId" + ], + "schemaTo": "public", + "tableTo": "IntegrationWhatsapp", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AdsConversionRule_k99JXbMObQIn_fkey", + "entityType": "fks", + "schema": "public", + "table": "AdsConversionRule" + }, + { + "nameExplicit": false, + "columns": [ + "integrationFacebookAdsId" + ], + "schemaTo": "public", + "tableTo": "IntegrationFacebookAds", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AdsConversionRule_fBmu8W26Mw5R_fkey", + "entityType": "fks", + "schema": "public", + "table": "AdsConversionRule" + }, + { + "nameExplicit": false, + "columns": [ + "integrationMessengerId" + ], + "schemaTo": "public", + "tableTo": "IntegrationMessenger", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AdsConversionRule_3zzAptVRRuZD_fkey", + "entityType": "fks", + "schema": "public", + "table": "AdsConversionRule" + }, + { + "nameExplicit": false, + "columns": [ + "integrationInstagramId" + ], + "schemaTo": "public", + "tableTo": "IntegrationInstagram", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AdsConversionRule_HUibBtSUZbML_fkey", + "entityType": "fks", + "schema": "public", + "table": "AdsConversionRule" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AIAgent_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AIAgent" + }, + { + "nameExplicit": false, + "columns": [ + "sourceId" + ], + "schemaTo": "public", + "tableTo": "AIConversationSource", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AIConversationEmbedding_sourceId_AIConversationSource_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AIConversationEmbedding" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AIConversationEmbedding_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AIConversationEmbedding" + }, + { + "nameExplicit": false, + "columns": [ + "conversationId" + ], + "schemaTo": "public", + "tableTo": "Conversation", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AIConversationEmbedding_conversationId_Conversation_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AIConversationEmbedding" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AIConversationSource_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AIConversationSource" + }, + { + "nameExplicit": false, + "columns": [ + "conversationId" + ], + "schemaTo": "public", + "tableTo": "Conversation", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AIConversationSource_conversationId_Conversation_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AIConversationSource" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AIEmbedding_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AIEmbedding" + }, + { + "nameExplicit": false, + "columns": [ + "aiFileId" + ], + "schemaTo": "public", + "tableTo": "AIFile", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AIEmbedding_aiFileId_AIFile_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AIEmbedding" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AIFile_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AIFile" + }, + { + "nameExplicit": false, + "columns": [ + "triggerFlowId" + ], + "schemaTo": "public", + "tableTo": "Flow", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "AIFunction_triggerFlowId_Flow_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AIFunction" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AIFunction_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AIFunction" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AIMCPServer_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AIMCPServer" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "AnalyticsBotMessageEvent_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AnalyticsBotMessageEvent" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "AnalyticsBroadcastEvent_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AnalyticsBroadcastEvent" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "AnalyticsContactEvent_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AnalyticsContactEvent" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "AnalyticsConversationEvent_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AnalyticsConversationEvent" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "AnalyticsFlowNodeEvent_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AnalyticsFlowNodeEvent" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "AnalyticsMessageEvent_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AnalyticsMessageEvent" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "AnalyticsSequenceEvent_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AnalyticsSequenceEvent" + }, + { + "nameExplicit": false, + "columns": [ + "topicId" + ], + "schemaTo": "public", + "tableTo": "EmailTopic", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AnalyticsEmailTopic_topicId_EmailTopic_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AnalyticsEmailTopic_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "nameExplicit": false, + "columns": [ + "contactId" + ], + "schemaTo": "public", + "tableTo": "Contact", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "AnalyticsEmailTopic_contactId_Contact_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "nameExplicit": false, + "columns": [ + "conversationId" + ], + "schemaTo": "public", + "tableTo": "Conversation", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "AnalyticsEmailTopic_conversationId_Conversation_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "nameExplicit": false, + "columns": [ + "contactInboxId" + ], + "schemaTo": "public", + "tableTo": "ContactInbox", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "AnalyticsEmailTopic_contactInboxId_ContactInbox_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Appointment_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Appointment" + }, + { + "nameExplicit": false, + "columns": [ + "calendarId" + ], + "schemaTo": "public", + "tableTo": "AppointmentCalendar", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "Appointment_calendarId_AppointmentCalendar_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Appointment" + }, + { + "nameExplicit": false, + "columns": [ + "contactId" + ], + "schemaTo": "public", + "tableTo": "Contact", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Appointment_contactId_Contact_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Appointment" + }, + { + "nameExplicit": false, + "columns": [ + "conversationId" + ], + "schemaTo": "public", + "tableTo": "Conversation", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "Appointment_conversationId_Conversation_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Appointment" + }, + { + "nameExplicit": false, + "columns": [ + "contactInboxId" + ], + "schemaTo": "public", + "tableTo": "ContactInbox", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "Appointment_contactInboxId_ContactInbox_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Appointment" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AppointmentCalendar_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "nameExplicit": false, + "columns": [ + "confirmationFlowId" + ], + "schemaTo": "public", + "tableTo": "Flow", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "AppointmentCalendar_confirmationFlowId_Flow_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "nameExplicit": false, + "columns": [ + "cancellationFlowId" + ], + "schemaTo": "public", + "tableTo": "Flow", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "AppointmentCalendar_cancellationFlowId_Flow_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "nameExplicit": false, + "columns": [ + "externalConnectionId" + ], + "schemaTo": "public", + "tableTo": "Integration", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "AppointmentCalendar_externalConnectionId_Integration_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "nameExplicit": false, + "columns": [ + "calendarId" + ], + "schemaTo": "public", + "tableTo": "AppointmentCalendar", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AppointmentCalendarAvailability_QfahoIQAX592_fkey", + "entityType": "fks", + "schema": "public", + "table": "AppointmentCalendarAvailability" + }, + { + "nameExplicit": false, + "columns": [ + "calendarId" + ], + "schemaTo": "public", + "tableTo": "AppointmentCalendar", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AppointmentCalendarReminder_5O4INfYC3dY3_fkey", + "entityType": "fks", + "schema": "public", + "table": "AppointmentCalendarReminder" + }, + { + "nameExplicit": false, + "columns": [ + "flowId" + ], + "schemaTo": "public", + "tableTo": "Flow", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AppointmentCalendarReminder_flowId_Flow_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AppointmentCalendarReminder" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AppointmentReminderDispatch_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AppointmentReminderDispatch" + }, + { + "nameExplicit": false, + "columns": [ + "appointmentId" + ], + "schemaTo": "public", + "tableTo": "Appointment", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AppointmentReminderDispatch_appointmentId_Appointment_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AppointmentReminderDispatch" + }, + { + "nameExplicit": false, + "columns": [ + "reminderConfigId" + ], + "schemaTo": "public", + "tableTo": "AppointmentCalendarReminder", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AppointmentReminderDispatch_a526jJM55OD7_fkey", + "entityType": "fks", + "schema": "public", + "table": "AppointmentReminderDispatch" + }, + { + "nameExplicit": false, + "columns": [ + "contactInboxId" + ], + "schemaTo": "public", + "tableTo": "ContactInbox", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "AppointmentReminderDispatch_contactInboxId_ContactInbox_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AppointmentReminderDispatch" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "User", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Account_userId_User_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Account" + }, + { + "nameExplicit": false, + "columns": [ + "tenantId" + ], + "schemaTo": "public", + "tableTo": "Tenant", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "Account_tenantId_Tenant_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Account" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Invitation_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Invitation" + }, + { + "nameExplicit": false, + "columns": [ + "invitedBy" + ], + "schemaTo": "public", + "tableTo": "User", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Invitation_invitedBy_User_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Invitation" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "User", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Session_userId_User_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Session" + }, + { + "nameExplicit": false, + "columns": [ + "tenantId" + ], + "schemaTo": "public", + "tableTo": "Tenant", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "User_tenantId_Tenant_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "User" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AutomatedResponse_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AutomatedResponse" + }, + { + "nameExplicit": false, + "columns": [ + "folderId" + ], + "schemaTo": "public", + "tableTo": "Folder", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "AutomatedResponse_folderId_Folder_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AutomatedResponse" + }, + { + "nameExplicit": false, + "columns": [ + "flowId" + ], + "schemaTo": "public", + "tableTo": "Flow", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "AutomatedResponse_flowId_Flow_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AutomatedResponse" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AutomationThrottle_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AutomationThrottle" + }, + { + "nameExplicit": false, + "columns": [ + "contactInboxId" + ], + "schemaTo": "public", + "tableTo": "ContactInbox", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AutomationThrottle_contactInboxId_ContactInbox_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AutomationThrottle" + }, + { + "nameExplicit": false, + "columns": [ + "folderId" + ], + "schemaTo": "public", + "tableTo": "Folder", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "BotField_folderId_Folder_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "BotField" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "BotField_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "BotField" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Broadcast_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Broadcast" + }, + { + "nameExplicit": false, + "columns": [ + "flowId" + ], + "schemaTo": "public", + "tableTo": "Flow", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Broadcast_flowId_Flow_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Broadcast" + }, + { + "nameExplicit": false, + "columns": [ + "integrationWhatsappId" + ], + "schemaTo": "public", + "tableTo": "IntegrationWhatsapp", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "Broadcast_integrationWhatsappId_IntegrationWhatsapp_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Broadcast" + }, + { + "nameExplicit": false, + "columns": [ + "integrationMessengerId" + ], + "schemaTo": "public", + "tableTo": "IntegrationMessenger", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "Broadcast_integrationMessengerId_IntegrationMessenger_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Broadcast" + }, + { + "nameExplicit": false, + "columns": [ + "broadcastId" + ], + "schemaTo": "public", + "tableTo": "Broadcast", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "BroadcastTarget_broadcastId_Broadcast_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "BroadcastTarget" + }, + { + "nameExplicit": false, + "columns": [ + "inboxId" + ], + "schemaTo": "public", + "tableTo": "Inbox", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "BroadcastTarget_inboxId_Inbox_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "BroadcastTarget" + }, + { + "nameExplicit": false, + "columns": [ + "flowId" + ], + "schemaTo": "public", + "tableTo": "Flow", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "BroadcastTarget_flowId_Flow_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "BroadcastTarget" + }, + { + "nameExplicit": false, + "columns": [ + "requestedByUserId" + ], + "schemaTo": "public", + "tableTo": "User", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "CoexistSyncRun_requestedByUserId_User_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Contact_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Contact" + }, + { + "nameExplicit": false, + "columns": [ + "contactId" + ], + "schemaTo": "public", + "tableTo": "Contact", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ContactCustomField_contactId_Contact_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ContactCustomField" + }, + { + "nameExplicit": false, + "columns": [ + "customFieldId" + ], + "schemaTo": "public", + "tableTo": "CustomField", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ContactCustomField_customFieldId_CustomField_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ContactCustomField" + }, + { + "nameExplicit": false, + "columns": [ + "contactId" + ], + "schemaTo": "public", + "tableTo": "Contact", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ContactInbox_contactId_Contact_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ContactInbox" + }, + { + "nameExplicit": false, + "columns": [ + "inboxId" + ], + "schemaTo": "public", + "tableTo": "Inbox", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ContactInbox_inboxId_Inbox_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ContactInbox" + }, + { + "nameExplicit": false, + "columns": [ + "contactId" + ], + "schemaTo": "public", + "tableTo": "Contact", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ContactNote_contactId_Contact_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ContactNote" + }, + { + "nameExplicit": false, + "columns": [ + "createdById" + ], + "schemaTo": "public", + "tableTo": "User", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ContactNote_createdById_User_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ContactNote" + }, + { + "nameExplicit": false, + "columns": [ + "broadcastId" + ], + "schemaTo": "public", + "tableTo": "Broadcast", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ContactOnBroadcast_broadcastId_Broadcast_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ContactOnBroadcast" + }, + { + "nameExplicit": false, + "columns": [ + "contactId" + ], + "schemaTo": "public", + "tableTo": "Contact", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ContactOnBroadcast_contactId_Contact_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ContactOnBroadcast" + }, + { + "nameExplicit": false, + "columns": [ + "contactInboxId" + ], + "schemaTo": "public", + "tableTo": "ContactInbox", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ContactOnBroadcast_contactInboxId_ContactInbox_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ContactOnBroadcast" + }, + { + "nameExplicit": false, + "columns": [ + "conversationId" + ], + "schemaTo": "public", + "tableTo": "Conversation", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ContactOnBroadcast_conversationId_Conversation_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ContactOnBroadcast" + }, + { + "nameExplicit": false, + "columns": [ + "contactId" + ], + "schemaTo": "public", + "tableTo": "Contact", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ContactOnSequence_contactId_Contact_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "nameExplicit": false, + "columns": [ + "sequenceId" + ], + "schemaTo": "public", + "tableTo": "Sequence", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ContactOnSequence_sequenceId_Sequence_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ContactOnSequence_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ContactOnSmartDelay_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ContactOnSmartDelay" + }, + { + "nameExplicit": false, + "columns": [ + "appointmentId" + ], + "schemaTo": "public", + "tableTo": "Appointment", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "ContactOnSmartDelay_appointmentId_Appointment_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ContactOnSmartDelay" + }, + { + "nameExplicit": false, + "columns": [ + "conversationId" + ], + "schemaTo": "public", + "tableTo": "Conversation", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ContactOnSmartDelay_conversationId_Conversation_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ContactOnSmartDelay" + }, + { + "nameExplicit": false, + "columns": [ + "contactId" + ], + "schemaTo": "public", + "tableTo": "Contact", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ContactToTag_contactId_Contact_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ContactToTag" + }, + { + "nameExplicit": false, + "columns": [ + "tagId" + ], + "schemaTo": "public", + "tableTo": "Tag", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ContactToTag_tagId_Tag_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ContactToTag" + }, + { + "nameExplicit": false, + "columns": [ + "tagId" + ], + "schemaTo": "public", + "tableTo": "Tag", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ContactToTagChannel_tagId_Tag_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ContactToTagChannel" + }, + { + "nameExplicit": false, + "columns": [ + "tagChannelId" + ], + "schemaTo": "public", + "tableTo": "TagChannel", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ContactToTagChannel_tagChannelId_TagChannel_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ContactToTagChannel" + }, + { + "nameExplicit": false, + "columns": [ + "contactInboxId" + ], + "schemaTo": "public", + "tableTo": "ContactInbox", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ContactToTagChannel_contactInboxId_ContactInbox_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ContactToTagChannel" + }, + { + "nameExplicit": false, + "columns": [ + "assignedUserId" + ], + "schemaTo": "public", + "tableTo": "User", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "Conversation_assignedUserId_User_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Conversation" + }, + { + "nameExplicit": false, + "columns": [ + "assignedInboxTeamId" + ], + "schemaTo": "public", + "tableTo": "InboxTeam", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "Conversation_assignedInboxTeamId_InboxTeam_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Conversation" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Conversation_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Conversation" + }, + { + "nameExplicit": false, + "columns": [ + "contactId" + ], + "schemaTo": "public", + "tableTo": "Contact", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Conversation_contactId_Contact_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Conversation" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ConversationParticipant_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ConversationParticipant" + }, + { + "nameExplicit": false, + "columns": [ + "conversationId" + ], + "schemaTo": "public", + "tableTo": "Conversation", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ConversationParticipant_conversationId_Conversation_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ConversationParticipant" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "User", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ConversationParticipant_userId_User_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ConversationParticipant" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Coupon_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Coupon" + }, + { + "nameExplicit": false, + "columns": [ + "topicId" + ], + "schemaTo": "public", + "tableTo": "CouponTopic", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Coupon_topicId_CouponTopic_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Coupon" + }, + { + "nameExplicit": false, + "columns": [ + "issuedContactId" + ], + "schemaTo": "public", + "tableTo": "Contact", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "Coupon_issuedContactId_Contact_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Coupon" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "CouponTopic_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "CouponTopic" + }, + { + "nameExplicit": false, + "columns": [ + "createdById" + ], + "schemaTo": "public", + "tableTo": "User", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "CouponTopic_createdById_User_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "CouponTopic" + }, + { + "nameExplicit": false, + "columns": [ + "folderId" + ], + "schemaTo": "public", + "tableTo": "Folder", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "CustomField_folderId_Folder_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "CustomField" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "CustomField_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "CustomField" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "DynamicImage_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "DynamicImage" + }, + { + "nameExplicit": false, + "columns": [ + "customFieldId" + ], + "schemaTo": "public", + "tableTo": "CustomField", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "DynamicImage_customFieldId_CustomField_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "DynamicImage" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "EmailTopic_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "EmailTopic" + }, + { + "nameExplicit": false, + "columns": [ + "folderId" + ], + "schemaTo": "public", + "tableTo": "Folder", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "EmailTopic_folderId_Folder_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "EmailTopic" + }, + { + "nameExplicit": true, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AuditLog_workspaceId_fkey", + "entityType": "fks", + "schema": "public", + "table": "AuditLog" + }, + { + "nameExplicit": true, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "User", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "AuditLog_userId_fkey", + "entityType": "fks", + "schema": "public", + "table": "AuditLog" + }, + { + "nameExplicit": false, + "columns": [ + "tenantId" + ], + "schemaTo": "public", + "tableTo": "Tenant", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "CustomDomain_tenantId_Tenant_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "CustomDomain" + }, + { + "nameExplicit": false, + "columns": [ + "ownerId" + ], + "schemaTo": "public", + "tableTo": "User", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "Tenant_ownerId_User_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Tenant" + }, + { + "nameExplicit": false, + "columns": [ + "tenantId" + ], + "schemaTo": "public", + "tableTo": "Tenant", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "TenantHelpItem_tenantId_Tenant_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "TenantHelpItem" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "User", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "UserQuota_userId_User_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "UserQuota" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "WorkspaceUsage_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "WorkspaceUsage" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ErrorLog_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ErrorLog" + }, + { + "nameExplicit": false, + "columns": [ + "contactId" + ], + "schemaTo": "public", + "tableTo": "Contact", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "ErrorLog_contactId_Contact_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ErrorLog" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ExternalWebhook_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ExternalWebhook" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "FacebookLeadAdsAutomation_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "FacebookLeadAdsAutomation" + }, + { + "nameExplicit": false, + "columns": [ + "flowId" + ], + "schemaTo": "public", + "tableTo": "Flow", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "FacebookLeadAdsAutomation_flowId_Flow_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "FacebookLeadAdsAutomation" + }, + { + "nameExplicit": false, + "columns": [ + "automationId" + ], + "schemaTo": "public", + "tableTo": "FacebookLeadAdsAutomation", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "FacebookLeadAdsLead_n8swD949nr3L_fkey", + "entityType": "fks", + "schema": "public", + "table": "FacebookLeadAdsLead" + }, + { + "nameExplicit": false, + "columns": [ + "contactId" + ], + "schemaTo": "public", + "tableTo": "Contact", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "FacebookLeadAdsLead_contactId_Contact_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "FacebookLeadAdsLead" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "FBCommentAutomation_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "nameExplicit": false, + "columns": [ + "folderId" + ], + "schemaTo": "public", + "tableTo": "Folder", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "FBCommentAutomation_folderId_Folder_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "FBCommentAutomationEvent_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "nameExplicit": false, + "columns": [ + "automationId" + ], + "schemaTo": "public", + "tableTo": "FBCommentAutomation", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "FBCommentAutomationEvent_LOfdyJGW0vJy_fkey", + "entityType": "fks", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "nameExplicit": false, + "columns": [ + "contactId" + ], + "schemaTo": "public", + "tableTo": "Contact", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "FBCommentAutomationEvent_contactId_Contact_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "nameExplicit": false, + "columns": [ + "automationId" + ], + "schemaTo": "public", + "tableTo": "FBCommentAutomation", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "FBCommentAutomationReply_Q6yXIfuQcsD0_fkey", + "entityType": "fks", + "schema": "public", + "table": "FBCommentAutomationReply" + }, + { + "nameExplicit": false, + "columns": [ + "contactId" + ], + "schemaTo": "public", + "tableTo": "Contact", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "FBCommentAutomationReply_contactId_Contact_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "FBCommentAutomationReply" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "FBCommentAutomationReply_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "FBCommentAutomationReply" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "File_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "File" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "User", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "File_userId_User_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "File" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Flow_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Flow" + }, + { + "nameExplicit": false, + "columns": [ + "folderId" + ], + "schemaTo": "public", + "tableTo": "Folder", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "Flow_folderId_Folder_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Flow" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "FlowNodeStat_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "FlowNodeStat" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "FlowRun_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "FlowRun" + }, + { + "nameExplicit": false, + "columns": [ + "flowId" + ], + "schemaTo": "public", + "tableTo": "Flow", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "FlowRun_flowId_Flow_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "FlowRun" + }, + { + "nameExplicit": false, + "columns": [ + "flowVersionId" + ], + "schemaTo": "public", + "tableTo": "FlowVersion", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "FlowRun_flowVersionId_FlowVersion_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "FlowRun" + }, + { + "nameExplicit": false, + "columns": [ + "conversationId" + ], + "schemaTo": "public", + "tableTo": "Conversation", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "FlowRun_conversationId_Conversation_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "FlowRun" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "FlowVersion_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "FlowVersion" + }, + { + "nameExplicit": false, + "columns": [ + "flowId" + ], + "schemaTo": "public", + "tableTo": "Flow", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "FlowVersion_flowId_Flow_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "FlowVersion" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Folder_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Folder" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IgStoryAutomation_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IgStoryAutomation" + }, + { + "nameExplicit": false, + "columns": [ + "folderId" + ], + "schemaTo": "public", + "tableTo": "Folder", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "IgStoryAutomation_folderId_Folder_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IgStoryAutomation" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Import_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Import" + }, + { + "nameExplicit": false, + "columns": [ + "inboxId" + ], + "schemaTo": "public", + "tableTo": "Inbox", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Import_inboxId_Inbox_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Import" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "User", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "Import_userId_User_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Import" + }, + { + "nameExplicit": false, + "columns": [ + "fileId" + ], + "schemaTo": "public", + "tableTo": "File", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "Import_fileId_File_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Import" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Inbox_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Inbox" + }, + { + "nameExplicit": false, + "columns": [ + "inboxId" + ], + "schemaTo": "public", + "tableTo": "Inbox", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "InboxContactStat_inboxId_Inbox_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "InboxContactStat" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "InboxTeam_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "InboxTeam" + }, + { + "nameExplicit": false, + "columns": [ + "inboxTeamId" + ], + "schemaTo": "public", + "tableTo": "InboxTeam", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "InboxTeamMember_inboxTeamId_InboxTeam_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "InboxTeamMember" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "User", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "InboxTeamMember_userId_User_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "InboxTeamMember" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationActiveCampaign_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationActiveCampaign" + }, + { + "nameExplicit": false, + "columns": [ + "integrationId" + ], + "schemaTo": "public", + "tableTo": "Integration", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationActiveCampaign_integrationId_Integration_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationActiveCampaign" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationApi_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationApi" + }, + { + "nameExplicit": false, + "columns": [ + "inboxId" + ], + "schemaTo": "public", + "tableTo": "Inbox", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationApi_inboxId_Inbox_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationApi" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Integration_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Integration" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationClaude_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationClaude" + }, + { + "nameExplicit": false, + "columns": [ + "integrationId" + ], + "schemaTo": "public", + "tableTo": "Integration", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationClaude_integrationId_Integration_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationClaude" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationDeepseek_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationDeepseek" + }, + { + "nameExplicit": false, + "columns": [ + "integrationId" + ], + "schemaTo": "public", + "tableTo": "Integration", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationDeepseek_integrationId_Integration_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationDeepseek" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationDrip_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationDrip" + }, + { + "nameExplicit": false, + "columns": [ + "integrationId" + ], + "schemaTo": "public", + "tableTo": "Integration", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationDrip_integrationId_Integration_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationDrip" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationFacebookAds_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationFacebookAds" + }, + { + "nameExplicit": false, + "columns": [ + "integrationId" + ], + "schemaTo": "public", + "tableTo": "Integration", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationFacebookAds_integrationId_Integration_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationFacebookAds" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationGemini_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationGemini" + }, + { + "nameExplicit": false, + "columns": [ + "integrationId" + ], + "schemaTo": "public", + "tableTo": "Integration", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationGemini_integrationId_Integration_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationGemini" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationGetResponse_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationGetResponse" + }, + { + "nameExplicit": false, + "columns": [ + "integrationId" + ], + "schemaTo": "public", + "tableTo": "Integration", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationGetResponse_integrationId_Integration_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationGetResponse" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationGoogleCalendar_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationGoogleCalendar" + }, + { + "nameExplicit": false, + "columns": [ + "integrationId" + ], + "schemaTo": "public", + "tableTo": "Integration", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationGoogleCalendar_integrationId_Integration_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationGoogleCalendar" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationGoogleSheet_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationGoogleSheet" + }, + { + "nameExplicit": false, + "columns": [ + "integrationId" + ], + "schemaTo": "public", + "tableTo": "Integration", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationGoogleSheet_integrationId_Integration_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationGoogleSheet" + }, + { + "nameExplicit": true, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationInstagram_workspaceId_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "nameExplicit": true, + "columns": [ + "inboxId" + ], + "schemaTo": "public", + "tableTo": "Inbox", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationInstagram_inboxId_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "nameExplicit": true, + "columns": [ + "welcomeFlowId" + ], + "schemaTo": "public", + "tableTo": "Flow", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "IntegrationInstagram_welcomeFlowId_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationKlaviyo_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationKlaviyo" + }, + { + "nameExplicit": false, + "columns": [ + "integrationId" + ], + "schemaTo": "public", + "tableTo": "Integration", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationKlaviyo_integrationId_Integration_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationKlaviyo" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationMailchimp_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationMailchimp" + }, + { + "nameExplicit": false, + "columns": [ + "integrationId" + ], + "schemaTo": "public", + "tableTo": "Integration", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationMailchimp_integrationId_Integration_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationMailchimp" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationMailerLite_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationMailerLite" + }, + { + "nameExplicit": false, + "columns": [ + "integrationId" + ], + "schemaTo": "public", + "tableTo": "Integration", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationMailerLite_integrationId_Integration_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationMailerLite" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationMessenger_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "nameExplicit": false, + "columns": [ + "inboxId" + ], + "schemaTo": "public", + "tableTo": "Inbox", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationMessenger_inboxId_Inbox_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "nameExplicit": false, + "columns": [ + "welcomeFlowId" + ], + "schemaTo": "public", + "tableTo": "Flow", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "IntegrationMessenger_welcomeFlowId_Flow_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationMetaCatalog_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "nameExplicit": false, + "columns": [ + "integrationId" + ], + "schemaTo": "public", + "tableTo": "Integration", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationMetaCatalog_integrationId_Integration_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationMoosend_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationMoosend" + }, + { + "nameExplicit": false, + "columns": [ + "integrationId" + ], + "schemaTo": "public", + "tableTo": "Integration", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationMoosend_integrationId_Integration_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationMoosend" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationOpenai_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationOpenai" + }, + { + "nameExplicit": false, + "columns": [ + "integrationId" + ], + "schemaTo": "public", + "tableTo": "Integration", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationOpenai_integrationId_Integration_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationOpenai" + }, + { + "nameExplicit": false, + "columns": [ + "aiAgentId" + ], + "schemaTo": "public", + "tableTo": "AIAgent", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "IntegrationOpenai_aiAgentId_AIAgent_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationOpenai" + }, + { + "nameExplicit": false, + "columns": [ + "integrationId" + ], + "schemaTo": "public", + "tableTo": "Integration", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationOpenaiCompatible_integrationId_Integration_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationOpenaiCompatible" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationOpenaiCompatible_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationOpenaiCompatible" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationOpenrouter_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationOpenrouter" + }, + { + "nameExplicit": false, + "columns": [ + "integrationId" + ], + "schemaTo": "public", + "tableTo": "Integration", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationOpenrouter_integrationId_Integration_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationOpenrouter" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationOutlookCalendar_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationOutlookCalendar" + }, + { + "nameExplicit": false, + "columns": [ + "integrationId" + ], + "schemaTo": "public", + "tableTo": "Integration", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationOutlookCalendar_integrationId_Integration_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationOutlookCalendar" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationSendGrid_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationSendGrid" + }, + { + "nameExplicit": false, + "columns": [ + "integrationId" + ], + "schemaTo": "public", + "tableTo": "Integration", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationSendGrid_integrationId_Integration_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationSendGrid" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationSmtp_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationSmtp" + }, + { + "nameExplicit": false, + "columns": [ + "inboxId" + ], + "schemaTo": "public", + "tableTo": "Inbox", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationSmtp_inboxId_Inbox_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationSmtp" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationTelegram_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationTelegram" + }, + { + "nameExplicit": false, + "columns": [ + "inboxId" + ], + "schemaTo": "public", + "tableTo": "Inbox", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationTelegram_inboxId_Inbox_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationTelegram" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationTiktok_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationTiktok" + }, + { + "nameExplicit": false, + "columns": [ + "inboxId" + ], + "schemaTo": "public", + "tableTo": "Inbox", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationTiktok_inboxId_Inbox_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationTiktok" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationWebchat_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationWebchat" + }, + { + "nameExplicit": false, + "columns": [ + "inboxId" + ], + "schemaTo": "public", + "tableTo": "Inbox", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationWebchat_inboxId_Inbox_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationWebchat" + }, + { + "nameExplicit": false, + "columns": [ + "welcomeFlowId" + ], + "schemaTo": "public", + "tableTo": "Flow", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "IntegrationWebchat_welcomeFlowId_Flow_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationWebchat" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationWhatsapp_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "nameExplicit": false, + "columns": [ + "inboxId" + ], + "schemaTo": "public", + "tableTo": "Inbox", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationWhatsapp_inboxId_Inbox_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationZalo_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationZalo" + }, + { + "nameExplicit": false, + "columns": [ + "inboxId" + ], + "schemaTo": "public", + "tableTo": "Inbox", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationZalo_inboxId_Inbox_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationZalo" + }, + { + "nameExplicit": false, + "columns": [ + "fallbackFlowId" + ], + "schemaTo": "public", + "tableTo": "Flow", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "IntegrationZalo_fallbackFlowId_Flow_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationZalo" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "MagicLink_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "MagicLink" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "MagicLinkStat_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "MagicLinkStat" + }, + { + "nameExplicit": false, + "columns": [ + "linkId" + ], + "schemaTo": "public", + "tableTo": "MagicLink", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "MagicLinkStat_linkId_MagicLink_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "MagicLinkStat" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "MediaLibraryFile_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "MediaLibraryFile" + }, + { + "nameExplicit": false, + "columns": [ + "folderId" + ], + "schemaTo": "public", + "tableTo": "MediaLibraryFolder", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "MediaLibraryFile_folderId_MediaLibraryFolder_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "MediaLibraryFile" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "MediaLibraryFolder_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "MediaLibraryFolder" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "MessagingAdOperation_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "nameExplicit": false, + "columns": [ + "integrationWhatsappId" + ], + "schemaTo": "public", + "tableTo": "IntegrationWhatsapp", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "MessagingAdOperation_D2X0VlACjh0B_fkey", + "entityType": "fks", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "nameExplicit": false, + "columns": [ + "integrationMessengerId" + ], + "schemaTo": "public", + "tableTo": "IntegrationMessenger", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "MessagingAdOperation_DlCmuyYqclNt_fkey", + "entityType": "fks", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "nameExplicit": false, + "columns": [ + "integrationInstagramId" + ], + "schemaTo": "public", + "tableTo": "IntegrationInstagram", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "MessagingAdOperation_LwL2ykeZegup_fkey", + "entityType": "fks", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "nameExplicit": false, + "columns": [ + "createdBy" + ], + "schemaTo": "public", + "tableTo": "User", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "MessagingAdOperation_createdBy_User_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "MessagingAdsConnection_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "MessagingAdsConnection" + }, + { + "nameExplicit": false, + "columns": [ + "integrationWhatsappId" + ], + "schemaTo": "public", + "tableTo": "IntegrationWhatsapp", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "MessagingAdsConnection_Q2FrBLiP3QqD_fkey", + "entityType": "fks", + "schema": "public", + "table": "MessagingAdsConnection" + }, + { + "nameExplicit": false, + "columns": [ + "integrationMessengerId" + ], + "schemaTo": "public", + "tableTo": "IntegrationMessenger", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "MessagingAdsConnection_tLZC78O5pMBb_fkey", + "entityType": "fks", + "schema": "public", + "table": "MessagingAdsConnection" + }, + { + "nameExplicit": false, + "columns": [ + "integrationInstagramId" + ], + "schemaTo": "public", + "tableTo": "IntegrationInstagram", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "MessagingAdsConnection_4ubezEjd9jQX_fkey", + "entityType": "fks", + "schema": "public", + "table": "MessagingAdsConnection" + }, + { + "nameExplicit": false, + "columns": [ + "integrationMessengerId" + ], + "schemaTo": "public", + "tableTo": "IntegrationMessenger", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "MessengerMessageTemplate_x0Vv1d8cvLYN_fkey", + "entityType": "fks", + "schema": "public", + "table": "MessengerMessageTemplate" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "MetaCapiEvent_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "nameExplicit": false, + "columns": [ + "contactInboxId" + ], + "schemaTo": "public", + "tableTo": "ContactInbox", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "MetaCapiEvent_contactInboxId_ContactInbox_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "nameExplicit": false, + "columns": [ + "integrationMetaCatalogId" + ], + "schemaTo": "public", + "tableTo": "IntegrationMetaCatalog", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "MetaCatalogItem_wJXyKUssR08y_fkey", + "entityType": "fks", + "schema": "public", + "table": "MetaCatalogItem" + }, + { + "nameExplicit": false, + "columns": [ + "productId" + ], + "schemaTo": "public", + "tableTo": "Product", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "MetaCatalogItem_productId_Product_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "MetaCatalogItem" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "MetaCatalogSyncRun_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "nameExplicit": false, + "columns": [ + "integrationMetaCatalogId" + ], + "schemaTo": "public", + "tableTo": "IntegrationMetaCatalog", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "MetaCatalogSyncRun_8PnmCJ0uISUd_fkey", + "entityType": "fks", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "nameExplicit": false, + "columns": [ + "categoryId" + ], + "schemaTo": "public", + "tableTo": "ProductCategory", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "MetaCatalogSyncRun_categoryId_ProductCategory_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Minigame_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Minigame" + }, + { + "nameExplicit": false, + "columns": [ + "minigameId" + ], + "schemaTo": "public", + "tableTo": "Minigame", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "MinigameContact_minigameId_Minigame_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "MinigameContact" + }, + { + "nameExplicit": false, + "columns": [ + "contactId" + ], + "schemaTo": "public", + "tableTo": "Contact", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "MinigameContact_contactId_Contact_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "MinigameContact" + }, + { + "nameExplicit": false, + "columns": [ + "referrerContactId" + ], + "schemaTo": "public", + "tableTo": "Contact", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "MinigameContact_referrerContactId_Contact_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "MinigameContact" + }, + { + "nameExplicit": false, + "columns": [ + "contactInboxId" + ], + "schemaTo": "public", + "tableTo": "ContactInbox", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "MinigameContact_contactInboxId_ContactInbox_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "MinigameContact" + }, + { + "nameExplicit": false, + "columns": [ + "minigameId" + ], + "schemaTo": "public", + "tableTo": "Minigame", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "MinigamePlay_minigameId_Minigame_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "MinigamePlay" + }, + { + "nameExplicit": false, + "columns": [ + "contactId" + ], + "schemaTo": "public", + "tableTo": "Contact", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "MinigamePlay_contactId_Contact_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "MinigamePlay" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "User", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Credential_userId_User_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "PlatformCredential" + }, + { + "nameExplicit": false, + "columns": [ + "categoryId" + ], + "schemaTo": "public", + "tableTo": "ProductCategory", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "Product_categoryId_ProductCategory_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Product" + }, + { + "nameExplicit": false, + "columns": [ + "subcategoryId" + ], + "schemaTo": "public", + "tableTo": "ProductCategory", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "Product_subcategoryId_ProductCategory_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Product" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Product_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Product" + }, + { + "nameExplicit": false, + "columns": [ + "productId" + ], + "schemaTo": "public", + "tableTo": "Product", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ProductAddon_productId_Product_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ProductAddon" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ProductCategory_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ProductCategory" + }, + { + "nameExplicit": false, + "columns": [ + "parentId" + ], + "schemaTo": "public", + "tableTo": "ProductCategory", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ProductCategory_parentId_ProductCategory_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ProductCategory" + }, + { + "nameExplicit": false, + "columns": [ + "productId" + ], + "schemaTo": "public", + "tableTo": "Product", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ProductVariant_productId_Product_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ProductVariant" + }, + { + "nameExplicit": false, + "columns": [ + "productId" + ], + "schemaTo": "public", + "tableTo": "Product", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ProductVariantOption_productId_Product_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ProductVariantOption" + }, + { + "nameExplicit": false, + "columns": [ + "submissionId" + ], + "schemaTo": "public", + "tableTo": "QuestionnaireSubmission", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "QuestionnaireAnswer_l8clOEM7NsPl_fkey", + "entityType": "fks", + "schema": "public", + "table": "QuestionnaireAnswer" + }, + { + "nameExplicit": false, + "columns": [ + "questionId" + ], + "schemaTo": "public", + "tableTo": "QuestionnaireQuestion", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "QuestionnaireAnswer_questionId_QuestionnaireQuestion_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "QuestionnaireAnswer" + }, + { + "nameExplicit": false, + "columns": [ + "triggerFlowId" + ], + "schemaTo": "public", + "tableTo": "Flow", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "Questionnaire_triggerFlowId_Flow_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Questionnaire" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Questionnaire_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Questionnaire" + }, + { + "nameExplicit": false, + "columns": [ + "questionnaireId" + ], + "schemaTo": "public", + "tableTo": "Questionnaire", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "QuestionnaireQuestion_questionnaireId_Questionnaire_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "QuestionnaireQuestion" + }, + { + "nameExplicit": false, + "columns": [ + "customFieldId" + ], + "schemaTo": "public", + "tableTo": "CustomField", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "QuestionnaireQuestion_customFieldId_CustomField_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "QuestionnaireQuestion" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "QuestionnaireSubmission_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "QuestionnaireSubmission" + }, + { + "nameExplicit": false, + "columns": [ + "questionnaireId" + ], + "schemaTo": "public", + "tableTo": "Questionnaire", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "QuestionnaireSubmission_questionnaireId_Questionnaire_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "QuestionnaireSubmission" + }, + { + "nameExplicit": false, + "columns": [ + "contactId" + ], + "schemaTo": "public", + "tableTo": "Contact", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "QuestionnaireSubmission_contactId_Contact_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "QuestionnaireSubmission" + }, + { + "nameExplicit": false, + "columns": [ + "conversationId" + ], + "schemaTo": "public", + "tableTo": "Conversation", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "QuestionnaireSubmission_conversationId_Conversation_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "QuestionnaireSubmission" + }, + { + "nameExplicit": false, + "columns": [ + "currentQuestionId" + ], + "schemaTo": "public", + "tableTo": "QuestionnaireQuestion", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "QuestionnaireSubmission_mha0bYFxcV7A_fkey", + "entityType": "fks", + "schema": "public", + "table": "QuestionnaireSubmission" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "RefLinkStat_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "RefLinkStat" + }, + { + "nameExplicit": false, + "columns": [ + "linkId" + ], + "schemaTo": "public", + "tableTo": "Reflink", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "RefLinkStat_linkId_Reflink_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "RefLinkStat" + }, + { + "nameExplicit": false, + "columns": [ + "flowId" + ], + "schemaTo": "public", + "tableTo": "Flow", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Reflink_flowId_Flow_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Reflink" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Reflink_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Reflink" + }, + { + "nameExplicit": false, + "columns": [ + "customFieldId" + ], + "schemaTo": "public", + "tableTo": "CustomField", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "Reflink_customFieldId_CustomField_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Reflink" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "SavedReply_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "SavedReply" + }, + { + "nameExplicit": false, + "columns": [ + "folderId" + ], + "schemaTo": "public", + "tableTo": "Folder", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "Sequence_folderId_Folder_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Sequence" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Sequence_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Sequence" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "SequenceDispatch_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "nameExplicit": false, + "columns": [ + "sequenceId" + ], + "schemaTo": "public", + "tableTo": "Sequence", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "SequenceDispatch_sequenceId_Sequence_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "nameExplicit": false, + "columns": [ + "contactId" + ], + "schemaTo": "public", + "tableTo": "Contact", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "SequenceDispatch_contactId_Contact_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "nameExplicit": false, + "columns": [ + "contactInboxId" + ], + "schemaTo": "public", + "tableTo": "ContactInbox", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "SequenceDispatch_contactInboxId_ContactInbox_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "nameExplicit": false, + "columns": [ + "stepId" + ], + "schemaTo": "public", + "tableTo": "SequenceStep", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "SequenceDispatch_stepId_SequenceStep_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "nameExplicit": true, + "columns": [ + "enrollmentId", + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "ContactOnSequence", + "columnsTo": [ + "id", + "workspaceId" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "SequenceDispatch_enrollment_workspace_fkey", + "entityType": "fks", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "nameExplicit": false, + "columns": [ + "flowId" + ], + "schemaTo": "public", + "tableTo": "Flow", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "SequenceStep_flowId_Flow_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "SequenceStep" + }, + { + "nameExplicit": false, + "columns": [ + "sequenceId" + ], + "schemaTo": "public", + "tableTo": "Sequence", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "SequenceStep_sequenceId_Sequence_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "SequenceStep" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Spreadsheet_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Spreadsheet" + }, + { + "nameExplicit": false, + "columns": [ + "folderId" + ], + "schemaTo": "public", + "tableTo": "Folder", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "Tag_folderId_Folder_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Tag" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Tag_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Tag" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "TagChannel_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "TagChannel" + }, + { + "nameExplicit": false, + "columns": [ + "tagId" + ], + "schemaTo": "public", + "tableTo": "Tag", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "TagChannel_tagId_Tag_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "TagChannel" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Template_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Template" + }, + { + "nameExplicit": false, + "columns": [ + "tenantId" + ], + "schemaTo": "public", + "tableTo": "Tenant", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "Template_tenantId_Tenant_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Template" + }, + { + "nameExplicit": false, + "columns": [ + "createdBy" + ], + "schemaTo": "public", + "tableTo": "User", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "Template_createdBy_User_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Template" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "TemplateInstallation_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "nameExplicit": false, + "columns": [ + "templateId" + ], + "schemaTo": "public", + "tableTo": "Template", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "TemplateInstallation_templateId_Template_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "nameExplicit": false, + "columns": [ + "installFolderId" + ], + "schemaTo": "public", + "tableTo": "Folder", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "TemplateInstallation_installFolderId_Folder_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "nameExplicit": false, + "columns": [ + "installedBy" + ], + "schemaTo": "public", + "tableTo": "User", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "TemplateInstallation_installedBy_User_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "nameExplicit": false, + "columns": [ + "installationId" + ], + "schemaTo": "public", + "tableTo": "TemplateInstallation", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "TemplateInstalledResource_a0BqwWeO96EB_fkey", + "entityType": "fks", + "schema": "public", + "table": "TemplateInstalledResource" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "TemplateInstalledResource_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "TemplateInstalledResource" + }, + { + "nameExplicit": false, + "columns": [ + "folderId" + ], + "schemaTo": "public", + "tableTo": "Folder", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "Trigger_folderId_Folder_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Trigger" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Trigger_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Trigger" + }, + { + "nameExplicit": false, + "columns": [ + "triggerId" + ], + "schemaTo": "public", + "tableTo": "Trigger", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Condition_triggerId_Trigger_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Condition" + }, + { + "nameExplicit": false, + "columns": [ + "webhookId" + ], + "schemaTo": "public", + "tableTo": "Webhook", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Condition_webhookId_Webhook_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Condition" + }, + { + "nameExplicit": false, + "columns": [ + "triggerId" + ], + "schemaTo": "public", + "tableTo": "Trigger", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "TriggerContactHistory_triggerId_Trigger_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "TriggerContactHistory" + }, + { + "nameExplicit": false, + "columns": [ + "contactId" + ], + "schemaTo": "public", + "tableTo": "Contact", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "TriggerContactHistory_contactId_Contact_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "TriggerContactHistory" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "TriggerContactHistory_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "TriggerContactHistory" + }, + { + "nameExplicit": false, + "columns": [ + "triggerId" + ], + "schemaTo": "public", + "tableTo": "Trigger", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "TriggerExecution_triggerId_Trigger_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "TriggerExecution" + }, + { + "nameExplicit": false, + "columns": [ + "contactId" + ], + "schemaTo": "public", + "tableTo": "Contact", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "TriggerExecution_contactId_Contact_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "TriggerExecution" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "TriggerExecution_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "TriggerExecution" + }, + { + "nameExplicit": false, + "columns": [ + "triggerId" + ], + "schemaTo": "public", + "tableTo": "Trigger", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "TriggerStat_triggerId_Trigger_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "TriggerStat" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "TriggerStat_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "TriggerStat" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "User", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "UserDeviceToken_userId_User_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "UserDeviceToken" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "UserDeviceToken_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "UserDeviceToken" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "UserPersistentMenu_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "UserPersistentMenu" + }, + { + "nameExplicit": false, + "columns": [ + "folderId" + ], + "schemaTo": "public", + "tableTo": "Folder", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "Webhook_folderId_Folder_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Webhook" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Webhook_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Webhook" + }, + { + "nameExplicit": false, + "columns": [ + "webhookId" + ], + "schemaTo": "public", + "tableTo": "Webhook", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "WebhookExecution_webhookId_Webhook_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "WebhookExecution" + }, + { + "nameExplicit": false, + "columns": [ + "contactId" + ], + "schemaTo": "public", + "tableTo": "Contact", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "WebhookExecution_contactId_Contact_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "WebhookExecution" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "WebhookExecution_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "WebhookExecution" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "WhatsappBusinessAccount_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "WhatsappBusinessAccount" + }, + { + "nameExplicit": false, + "columns": [ + "integrationWhatsappId" + ], + "schemaTo": "public", + "tableTo": "IntegrationWhatsapp", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "WhatsappFlow_integrationWhatsappId_IntegrationWhatsapp_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "WhatsappFlow" + }, + { + "nameExplicit": false, + "columns": [ + "integrationWhatsappId" + ], + "schemaTo": "public", + "tableTo": "IntegrationWhatsapp", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "WhatsappMessageTemplate_p6pSomUTTJCm_fkey", + "entityType": "fks", + "schema": "public", + "table": "WhatsappMessageTemplate" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "User", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "WhatsappSignupSession_userId_User_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "WhatsappSignupSession" + }, + { + "nameExplicit": false, + "columns": [ + "ownerId" + ], + "schemaTo": "public", + "tableTo": "User", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "WhatsappSignupSession_ownerId_User_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "WhatsappSignupSession" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "WhatsappSignupSession_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "WhatsappSignupSession" + }, + { + "nameExplicit": false, + "columns": [ + "ownerId" + ], + "schemaTo": "public", + "tableTo": "User", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "Workspace_userId_User_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Workspace" + }, + { + "nameExplicit": false, + "columns": [ + "tenantId" + ], + "schemaTo": "public", + "tableTo": "Tenant", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "Workspace_tenantId_Tenant_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Workspace" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "WorkspaceApiToken_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "WorkspaceApiToken" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "WorkspaceMac_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "WorkspaceMac" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "WorkspaceMember_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "WorkspaceMember" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "User", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "WorkspaceMember_userId_User_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "WorkspaceMember" + }, + { + "nameExplicit": false, + "columns": [ + "contactInboxId" + ], + "schemaTo": "public", + "tableTo": "ContactInbox", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "FBCommentAutomationEvent_contactInboxId_ContactInbox_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "FBCommentAutomationMiss_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "FBCommentAutomationMiss" + }, + { + "nameExplicit": false, + "columns": [ + "automationId" + ], + "schemaTo": "public", + "tableTo": "FBCommentAutomation", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "FBCommentAutomationMiss_4mfgnR9RsQNo_fkey", + "entityType": "fks", + "schema": "public", + "table": "FBCommentAutomationMiss" + }, + { + "nameExplicit": false, + "columns": [ + "contactId" + ], + "schemaTo": "public", + "tableTo": "Contact", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "FBCommentAutomationMiss_contactId_Contact_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "FBCommentAutomationMiss" + }, + { + "nameExplicit": false, + "columns": [ + "contactInboxId" + ], + "schemaTo": "public", + "tableTo": "ContactInbox", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "FBCommentAutomationMiss_contactInboxId_ContactInbox_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "FBCommentAutomationMiss" + }, + { + "columns": [ + "occurredAt", + "eventId" + ], + "nameExplicit": false, + "name": "AnalyticsBotMessageEvent_pkey", + "entityType": "pks", + "schema": "public", + "table": "AnalyticsBotMessageEvent" + }, + { + "columns": [ + "broadcastId", + "contactInboxId", + "batchId", + "eventType", + "occurredAt" + ], + "nameExplicit": false, + "name": "AnalyticsBroadcastEvent_pkey", + "entityType": "pks", + "schema": "public", + "table": "AnalyticsBroadcastEvent" + }, + { + "columns": [ + "occurredAt", + "eventId" + ], + "nameExplicit": false, + "name": "AnalyticsContactEvent_pkey", + "entityType": "pks", + "schema": "public", + "table": "AnalyticsContactEvent" + }, + { + "columns": [ + "occurredAt", + "eventId" + ], + "nameExplicit": false, + "name": "AnalyticsConversationEvent_pkey", + "entityType": "pks", + "schema": "public", + "table": "AnalyticsConversationEvent" + }, + { + "columns": [ + "flowId", + "analyticsId", + "nodeId", + "buttonId", + "contactInboxId", + "eventType", + "occurredAt" + ], + "nameExplicit": false, + "name": "AnalyticsFlowNodeEvent_pkey", + "entityType": "pks", + "schema": "public", + "table": "AnalyticsFlowNodeEvent" + }, + { + "columns": [ + "occurredAt", + "eventId" + ], + "nameExplicit": false, + "name": "AnalyticsMessageEvent_pkey", + "entityType": "pks", + "schema": "public", + "table": "AnalyticsMessageEvent" + }, + { + "columns": [ + "sequenceId", + "stepId", + "contactInboxId", + "eventType", + "occurredAt" + ], + "nameExplicit": false, + "name": "AnalyticsSequenceEvent_pkey", + "entityType": "pks", + "schema": "public", + "table": "AnalyticsSequenceEvent" + }, + { + "columns": [ + "id", + "createdAt" + ], + "nameExplicit": false, + "name": "Attachment_pkey", + "entityType": "pks", + "schema": "public", + "table": "Attachment" + }, + { + "columns": [ + "workspaceId", + "contactInboxId", + "throttleType", + "subjectId" + ], + "nameExplicit": true, + "name": "AutomationThrottle_pkey", + "entityType": "pks", + "schema": "public", + "table": "AutomationThrottle" + }, + { + "columns": [ + "broadcastId", + "inboxId" + ], + "nameExplicit": true, + "name": "BroadcastTarget_pkey", + "entityType": "pks", + "schema": "public", + "table": "BroadcastTarget" + }, + { + "columns": [ + "workspaceId", + "hourBucket", + "contactInboxId" + ], + "nameExplicit": false, + "name": "ContactActiveHourly_pkey", + "entityType": "pks", + "schema": "public", + "table": "ContactActiveHourly" + }, + { + "columns": [ + "workspaceId", + "periodStart", + "contactInboxId" + ], + "nameExplicit": false, + "name": "ContactActiveMonthly_pkey", + "entityType": "pks", + "schema": "public", + "table": "ContactActiveMonthly" + }, + { + "columns": [ + "broadcastId", + "contactId" + ], + "nameExplicit": true, + "name": "ContactsOnBroadcast_pkey", + "entityType": "pks", + "schema": "public", + "table": "ContactOnBroadcast" + }, + { + "columns": [ + "id", + "workspaceId" + ], + "nameExplicit": true, + "name": "ContactOnSequence_pkey", + "entityType": "pks", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "columns": [ + "contactId", + "tagId" + ], + "nameExplicit": false, + "name": "ContactToTag_pkey", + "entityType": "pks", + "schema": "public", + "table": "ContactToTag" + }, + { + "columns": [ + "tagChannelId", + "contactInboxId" + ], + "nameExplicit": false, + "name": "ContactToTagChannel_pkey", + "entityType": "pks", + "schema": "public", + "table": "ContactToTagChannel" + }, + { + "columns": [ + "id", + "createdAt" + ], + "nameExplicit": false, + "name": "Message_pkey", + "entityType": "pks", + "schema": "public", + "table": "Message" + }, + { + "columns": [ + "id", + "workspaceId" + ], + "nameExplicit": true, + "name": "SequenceDispatch_pkey", + "entityType": "pks", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "columns": [ + "id", + "contactId" + ], + "nameExplicit": true, + "name": "TriggerContactHistory_pkey", + "entityType": "pks", + "schema": "public", + "table": "TriggerContactHistory" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "MessageShard_pkey", + "schema": "public", + "table": "MessageShard", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "ShardTimeRange_pkey", + "schema": "public", + "table": "ShardTimeRange", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "AdsConversionEvent_pkey", + "schema": "public", + "table": "AdsConversionEvent", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "AdsConversionRule_pkey", + "schema": "public", + "table": "AdsConversionRule", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "AIAgent_pkey", + "schema": "public", + "table": "AIAgent", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "AIConversationEmbedding_pkey", + "schema": "public", + "table": "AIConversationEmbedding", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "AIConversationSource_pkey", + "schema": "public", + "table": "AIConversationSource", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "AIEmbedding_pkey", + "schema": "public", + "table": "AIEmbedding", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "AIFile_pkey", + "schema": "public", + "table": "AIFile", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "AIFunction_pkey", + "schema": "public", + "table": "AIFunction", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "AIMCPServer_pkey", + "schema": "public", + "table": "AIMCPServer", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "AnalyticsEmailTopic_pkey", + "schema": "public", + "table": "AnalyticsEmailTopic", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Appointment_pkey", + "schema": "public", + "table": "Appointment", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "AppointmentCalendar_pkey", + "schema": "public", + "table": "AppointmentCalendar", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "AppointmentCalendarAvailability_pkey", + "schema": "public", + "table": "AppointmentCalendarAvailability", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "AppointmentCalendarReminder_pkey", + "schema": "public", + "table": "AppointmentCalendarReminder", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "AppointmentReminderDispatch_pkey", + "schema": "public", + "table": "AppointmentReminderDispatch", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Account_pkey", + "schema": "public", + "table": "Account", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Invitation_pkey", + "schema": "public", + "table": "Invitation", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Jwk_pkey", + "schema": "public", + "table": "Jwk", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Session_pkey", + "schema": "public", + "table": "Session", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "User_pkey", + "schema": "public", + "table": "User", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Verification_pkey", + "schema": "public", + "table": "Verification", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "AutomatedResponse_pkey", + "schema": "public", + "table": "AutomatedResponse", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "BotField_pkey", + "schema": "public", + "table": "BotField", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Broadcast_pkey", + "schema": "public", + "table": "Broadcast", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "CoexistSyncRun_pkey", + "schema": "public", + "table": "CoexistSyncRun", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Contact_pkey", + "schema": "public", + "table": "Contact", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "ContactCustomField_pkey", + "schema": "public", + "table": "ContactCustomField", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "ContactInbox_pkey", + "schema": "public", + "table": "ContactInbox", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "ContactNote_pkey", + "schema": "public", + "table": "ContactNote", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "ContactOnSmartDelay_pkey", + "schema": "public", + "table": "ContactOnSmartDelay", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Conversation_pkey", + "schema": "public", + "table": "Conversation", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "ConversationParticipant_pkey", + "schema": "public", + "table": "ConversationParticipant", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Coupon_pkey", + "schema": "public", + "table": "Coupon", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "CouponTopic_pkey", + "schema": "public", + "table": "CouponTopic", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "CustomField_pkey", + "schema": "public", + "table": "CustomField", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "DynamicImage_pkey", + "schema": "public", + "table": "DynamicImage", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "EmailTopic_pkey", + "schema": "public", + "table": "EmailTopic", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "AuditLog_pkey", + "schema": "public", + "table": "AuditLog", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "CustomDomain_pkey", + "schema": "public", + "table": "CustomDomain", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Tenant_pkey", + "schema": "public", + "table": "Tenant", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "TenantHelpItem_pkey", + "schema": "public", + "table": "TenantHelpItem", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "UserQuota_pkey", + "schema": "public", + "table": "UserQuota", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "WorkspaceUsage_pkey", + "schema": "public", + "table": "WorkspaceUsage", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "ErrorLog_pkey", + "schema": "public", + "table": "ErrorLog", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "ExternalWebhook_pkey", + "schema": "public", + "table": "ExternalWebhook", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "FacebookLeadAdsAutomation_pkey", + "schema": "public", + "table": "FacebookLeadAdsAutomation", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "FacebookLeadAdsLead_pkey", + "schema": "public", + "table": "FacebookLeadAdsLead", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "FBCommentAutomation_pkey", + "schema": "public", + "table": "FBCommentAutomation", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "FBCommentAutomationEvent_pkey", + "schema": "public", + "table": "FBCommentAutomationEvent", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "FBCommentAutomationReply_pkey", + "schema": "public", + "table": "FBCommentAutomationReply", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "File_pkey", + "schema": "public", + "table": "File", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Flow_pkey", + "schema": "public", + "table": "Flow", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "FlowAnalyticsSession_pkey", + "schema": "public", + "table": "FlowAnalyticsSession", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "FlowNodeStat_pkey", + "schema": "public", + "table": "FlowNodeStat", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "FlowRun_pkey", + "schema": "public", + "table": "FlowRun", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "FlowVersion_pkey", + "schema": "public", + "table": "FlowVersion", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Folder_pkey", + "schema": "public", + "table": "Folder", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IgStoryAutomation_pkey", + "schema": "public", + "table": "IgStoryAutomation", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Import_pkey", + "schema": "public", + "table": "Import", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Inbox_pkey", + "schema": "public", + "table": "Inbox", + "entityType": "pks" + }, + { + "columns": [ + "inboxId" + ], + "nameExplicit": false, + "name": "InboxContactStat_pkey", + "schema": "public", + "table": "InboxContactStat", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "InboxTeam_pkey", + "schema": "public", + "table": "InboxTeam", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "InboxTeamMember_pkey", + "schema": "public", + "table": "InboxTeamMember", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationActiveCampaign_pkey", + "schema": "public", + "table": "IntegrationActiveCampaign", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationApi_pkey", + "schema": "public", + "table": "IntegrationApi", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Integration_pkey", + "schema": "public", + "table": "Integration", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationClaude_pkey", + "schema": "public", + "table": "IntegrationClaude", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationDeepseek_pkey", + "schema": "public", + "table": "IntegrationDeepseek", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationDrip_pkey", + "schema": "public", + "table": "IntegrationDrip", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationFacebookAds_pkey", + "schema": "public", + "table": "IntegrationFacebookAds", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationGemini_pkey", + "schema": "public", + "table": "IntegrationGemini", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationGetResponse_pkey", + "schema": "public", + "table": "IntegrationGetResponse", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationGoogleCalendar_pkey", + "schema": "public", + "table": "IntegrationGoogleCalendar", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationGoogleSheet_pkey", + "schema": "public", + "table": "IntegrationGoogleSheet", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationInstagram_pkey", + "schema": "public", + "table": "IntegrationInstagram", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationKlaviyo_pkey", + "schema": "public", + "table": "IntegrationKlaviyo", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationMailchimp_pkey", + "schema": "public", + "table": "IntegrationMailchimp", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationMailerLite_pkey", + "schema": "public", + "table": "IntegrationMailerLite", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationMessenger_pkey", + "schema": "public", + "table": "IntegrationMessenger", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationMetaCatalog_pkey", + "schema": "public", + "table": "IntegrationMetaCatalog", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationMoosend_pkey", + "schema": "public", + "table": "IntegrationMoosend", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationOpenai_pkey", + "schema": "public", + "table": "IntegrationOpenai", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationOpenaiCompatible_pkey", + "schema": "public", + "table": "IntegrationOpenaiCompatible", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationOpenrouter_pkey", + "schema": "public", + "table": "IntegrationOpenrouter", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationOutlookCalendar_pkey", + "schema": "public", + "table": "IntegrationOutlookCalendar", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationSendGrid_pkey", + "schema": "public", + "table": "IntegrationSendGrid", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationSmtp_pkey", + "schema": "public", + "table": "IntegrationSmtp", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationTelegram_pkey", + "schema": "public", + "table": "IntegrationTelegram", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationTiktok_pkey", + "schema": "public", + "table": "IntegrationTiktok", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationWebchat_pkey", + "schema": "public", + "table": "IntegrationWebchat", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationWhatsapp_pkey", + "schema": "public", + "table": "IntegrationWhatsapp", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationZalo_pkey", + "schema": "public", + "table": "IntegrationZalo", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "MagicLink_pkey", + "schema": "public", + "table": "MagicLink", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "MediaLibraryFile_pkey", + "schema": "public", + "table": "MediaLibraryFile", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "MediaLibraryFolder_pkey", + "schema": "public", + "table": "MediaLibraryFolder", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "MessageCleanup_pkey", + "schema": "public", + "table": "MessageCleanup", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "MessagingAdOperation_pkey", + "schema": "public", + "table": "MessagingAdOperation", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "MessagingAdsConnection_pkey", + "schema": "public", + "table": "MessagingAdsConnection", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "MessengerMessageTemplate_pkey", + "schema": "public", + "table": "MessengerMessageTemplate", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "MetaCapiEvent_pkey", + "schema": "public", + "table": "MetaCapiEvent", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "MetaCatalogItem_pkey", + "schema": "public", + "table": "MetaCatalogItem", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "MetaCatalogSyncRun_pkey", + "schema": "public", + "table": "MetaCatalogSyncRun", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Minigame_pkey", + "schema": "public", + "table": "Minigame", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "MinigameContact_pkey", + "schema": "public", + "table": "MinigameContact", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "MinigamePlay_pkey", + "schema": "public", + "table": "MinigamePlay", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Credential_pkey", + "schema": "public", + "table": "PlatformCredential", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Product_pkey", + "schema": "public", + "table": "Product", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "ProductAddon_pkey", + "schema": "public", + "table": "ProductAddon", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "ProductCategory_pkey", + "schema": "public", + "table": "ProductCategory", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "ProductVariant_pkey", + "schema": "public", + "table": "ProductVariant", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "ProductVariantOption_pkey", + "schema": "public", + "table": "ProductVariantOption", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "QuestionnaireAnswer_pkey", + "schema": "public", + "table": "QuestionnaireAnswer", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Questionnaire_pkey", + "schema": "public", + "table": "Questionnaire", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "QuestionnaireQuestion_pkey", + "schema": "public", + "table": "QuestionnaireQuestion", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "QuestionnaireSubmission_pkey", + "schema": "public", + "table": "QuestionnaireSubmission", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Reflink_pkey", + "schema": "public", + "table": "Reflink", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "SavedReply_pkey", + "schema": "public", + "table": "SavedReply", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Sequence_pkey", + "schema": "public", + "table": "Sequence", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "SequenceStep_pkey", + "schema": "public", + "table": "SequenceStep", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Spreadsheet_pkey", + "schema": "public", + "table": "Spreadsheet", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "SystemField_pkey", + "schema": "public", + "table": "SystemField", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Tag_pkey", + "schema": "public", + "table": "Tag", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "TagChannel_pkey", + "schema": "public", + "table": "TagChannel", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Template_pkey", + "schema": "public", + "table": "Template", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "TemplateInstallation_pkey", + "schema": "public", + "table": "TemplateInstallation", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "TemplateInstalledResource_pkey", + "schema": "public", + "table": "TemplateInstalledResource", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Trigger_pkey", + "schema": "public", + "table": "Trigger", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Condition_pkey", + "schema": "public", + "table": "Condition", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "TriggerExecution_pkey", + "schema": "public", + "table": "TriggerExecution", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "TriggerStat_pkey", + "schema": "public", + "table": "TriggerStat", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "UserDeviceToken_pkey", + "schema": "public", + "table": "UserDeviceToken", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "UserPersistentMenu_pkey", + "schema": "public", + "table": "UserPersistentMenu", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Webhook_pkey", + "schema": "public", + "table": "Webhook", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "WebhookExecution_pkey", + "schema": "public", + "table": "WebhookExecution", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "WhatsappBusinessAccount_pkey", + "schema": "public", + "table": "WhatsappBusinessAccount", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "WhatsappCoexistStaging_pkey", + "schema": "public", + "table": "WhatsappCoexistStaging", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "WhatsappFlow_pkey", + "schema": "public", + "table": "WhatsappFlow", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "WhatsappMessageTemplate_pkey", + "schema": "public", + "table": "WhatsappMessageTemplate", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "WhatsappSignupSession_pkey", + "schema": "public", + "table": "WhatsappSignupSession", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Workspace_pkey", + "schema": "public", + "table": "Workspace", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "WorkspaceApiToken_pkey", + "schema": "public", + "table": "WorkspaceApiToken", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "WorkspaceMac_pkey", + "schema": "public", + "table": "WorkspaceMac", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "WorkspaceMember_pkey", + "schema": "public", + "table": "WorkspaceMember", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "FBCommentAutomationMiss_pkey", + "schema": "public", + "table": "FBCommentAutomationMiss", + "entityType": "pks" + }, + { + "nameExplicit": true, + "columns": [ + "contactInboxId", + "sourceId", + "createdAt" + ], + "nullsNotDistinct": false, + "name": "Message_source_dedup_idx", + "entityType": "uniques", + "schema": "public", + "table": "Message" + }, + { + "nameExplicit": true, + "columns": [ + "workspaceId", + "parentId", + "name" + ], + "nullsNotDistinct": true, + "name": "ProductCategory_workspaceId_parent_name_key", + "entityType": "uniques", + "schema": "public", + "table": "ProductCategory" + }, + { + "nameExplicit": true, + "columns": [ + "token" + ], + "nullsNotDistinct": false, + "name": "UserDeviceToken_token_key", + "entityType": "uniques", + "schema": "public", + "table": "UserDeviceToken" + }, + { + "nameExplicit": true, + "columns": [ + "tokenHash" + ], + "nullsNotDistinct": false, + "name": "WorkspaceApiToken_tokenHash_key", + "entityType": "uniques", + "schema": "public", + "table": "WorkspaceApiToken" + }, + { + "nameExplicit": true, + "columns": [ + "workspaceId", + "periodStart", + "periodEnd" + ], + "nullsNotDistinct": false, + "name": "WorkspaceMac_workspaceId_periodStart_periodEnd_unique", + "entityType": "uniques", + "schema": "public", + "table": "WorkspaceMac" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "nullsNotDistinct": false, + "name": "UserQuota_userId_key", + "schema": "public", + "table": "UserQuota", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "nullsNotDistinct": false, + "name": "WorkspaceUsage_workspaceId_key", + "schema": "public", + "table": "WorkspaceUsage", + "entityType": "uniques" + }, + { + "value": "(\"channel\" = 'whatsapp' AND \"integrationWhatsappId\" IS NOT NULL AND \"ctwaClid\" IS NOT NULL AND \"wabaId\" IS NOT NULL) OR (\"channel\" = 'messenger' AND \"integrationMessengerId\" IS NOT NULL) OR (\"channel\" = 'instagram' AND \"integrationInstagramId\" IS NOT NULL)", + "name": "AdsConversionEvent_channel_integration_check", + "entityType": "checks", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "value": "(\"registrationStatus\" <> 'failed' OR \"registrationError\" IS NOT NULL)\n AND (\"registrationStatus\" <> 'registered' OR \"registrationError\" IS NULL)", + "name": "IntegrationWhatsapp_registrationStatus_error_consistent", + "entityType": "checks", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "value": "(\"channel\" = 'whatsapp' AND \"integrationWhatsappId\" IS NOT NULL AND \"integrationMessengerId\" IS NULL AND \"integrationInstagramId\" IS NULL) OR (\"channel\" = 'messenger' AND \"integrationMessengerId\" IS NOT NULL AND \"integrationWhatsappId\" IS NULL AND \"integrationInstagramId\" IS NULL) OR (\"channel\" = 'instagram' AND \"integrationInstagramId\" IS NOT NULL AND \"integrationWhatsappId\" IS NULL AND \"integrationMessengerId\" IS NULL)", + "name": "MessagingAdOperation_channel_integration_check", + "entityType": "checks", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "value": "(\"channel\" = 'whatsapp' AND \"integrationWhatsappId\" IS NOT NULL AND \"integrationMessengerId\" IS NULL AND \"integrationInstagramId\" IS NULL) OR (\"channel\" = 'messenger' AND \"integrationMessengerId\" IS NOT NULL AND \"integrationWhatsappId\" IS NULL AND \"integrationInstagramId\" IS NULL) OR (\"channel\" = 'instagram' AND \"integrationInstagramId\" IS NOT NULL AND \"integrationWhatsappId\" IS NULL AND \"integrationMessengerId\" IS NULL)", + "name": "MessagingAdsConnection_channel_integration_check", + "entityType": "checks", + "schema": "public", + "table": "MessagingAdsConnection" + }, + { + "value": "\"channel\" IN ('messenger', 'instagram', 'whatsapp')", + "name": "MetaCapiEvent_channel_check", + "entityType": "checks", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "value": "\"actionSource\" IN ('business_messaging', 'email', 'phone_call', 'chat', 'physical_store', 'system_generated', 'other')", + "name": "MetaCapiEvent_actionSource_check", + "entityType": "checks", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "value": "\"contentType\" IN ('product', 'product_group')", + "name": "MetaCapiEvent_contentType_check", + "entityType": "checks", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "value": "(\"customFieldId\" IS NULL) OR (\"systemFieldKey\" IS NULL)", + "name": "QuestionnaireQuestion_customFieldId_systemFieldKey_exclusive", + "entityType": "checks", + "schema": "public", + "table": "QuestionnaireQuestion" + }, + { + "value": "cardinality(\"candidatePhoneNumberIds\") > 0", + "name": "WhatsappSignupSession_candidates_not_empty", + "entityType": "checks", + "schema": "public", + "table": "WhatsappSignupSession" + } + ], + "renames": [] +} diff --git a/packages/database/drizzle/20260914170910_add_connection_tables/migration.sql b/packages/database/drizzle/20260914170910_add_connection_tables/migration.sql new file mode 100644 index 0000000000..35e505f4ca --- /dev/null +++ b/packages/database/drizzle/20260914170910_add_connection_tables/migration.sql @@ -0,0 +1,65 @@ +CREATE TABLE "ConnectSession" ( + "id" bigint PRIMARY KEY, + "createdAt" timestamp(6) with time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp(6) with time zone DEFAULT now() NOT NULL, + "workspaceId" bigint NOT NULL, + "provider" text NOT NULL, + "purpose" text NOT NULL, + "targetConnectionId" bigint, + "actorUserId" bigint, + "actorTokenId" bigint, + "platformOwnerId" text, + "originHost" text, + "returnUrl" text, + "stateNonceHash" text NOT NULL, + "status" text DEFAULT 'pending' NOT NULL, + "step" text DEFAULT 'authorize' NOT NULL, + "nextAction" jsonb, + "encryptedAuth" jsonb, + "targets" jsonb NOT NULL, + "claimedTargetIds" text[] NOT NULL, + "resultConnectionIds" text[] NOT NULL, + "results" jsonb NOT NULL, + "errorCode" text, + "expiresAt" timestamp(6) with time zone NOT NULL, + "consumedAt" timestamp(6) with time zone, + CONSTRAINT "ConnectSession_actor_exactly_one" CHECK ((("actorUserId" IS NOT NULL)::int + ("actorTokenId" IS NOT NULL)::int) = 1) +); +--> statement-breakpoint +CREATE TABLE "Connection" ( + "id" bigint PRIMARY KEY, + "createdAt" timestamp(6) with time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp(6) with time zone DEFAULT now() NOT NULL, + "workspaceId" bigint NOT NULL, + "provider" text NOT NULL, + "kind" text NOT NULL, + "channel" text, + "inboxId" bigint, + "integrationId" bigint, + "sourceId" text NOT NULL, + "displayName" text NOT NULL, + "status" text DEFAULT 'connected' NOT NULL, + "statusReason" text, + "lastError" text, + "authExpiresAt" timestamp(6) with time zone, + "createdBy" bigint, + "connectedAt" timestamp(6) with time zone, + "disconnectedAt" timestamp(6) with time zone +); +--> statement-breakpoint +CREATE INDEX "ConnectSession_workspaceId_idx" ON "ConnectSession" ("workspaceId");--> statement-breakpoint +CREATE INDEX "ConnectSession_expiresAt_idx" ON "ConnectSession" ("expiresAt");--> statement-breakpoint +CREATE UNIQUE INDEX "ConnectSession_stateNonceHash_key" ON "ConnectSession" ("stateNonceHash");--> statement-breakpoint +CREATE INDEX "Connection_workspaceId_idx" ON "Connection" ("workspaceId");--> statement-breakpoint +CREATE UNIQUE INDEX "Connection_workspaceId_provider_sourceId_key" ON "Connection" ("workspaceId","provider","sourceId");--> statement-breakpoint +CREATE UNIQUE INDEX "Connection_inboxId_key" ON "Connection" ("inboxId");--> statement-breakpoint +CREATE UNIQUE INDEX "Connection_integrationId_key" ON "Connection" ("integrationId");--> statement-breakpoint +CREATE INDEX "Connection_status_authExpiresAt_idx" ON "Connection" ("status","authExpiresAt");--> statement-breakpoint +ALTER TABLE "ConnectSession" ADD CONSTRAINT "ConnectSession_workspaceId_Workspace_id_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE CASCADE ON UPDATE CASCADE;--> statement-breakpoint +ALTER TABLE "ConnectSession" ADD CONSTRAINT "ConnectSession_targetConnectionId_Connection_id_fkey" FOREIGN KEY ("targetConnectionId") REFERENCES "Connection"("id") ON DELETE SET NULL ON UPDATE CASCADE;--> statement-breakpoint +ALTER TABLE "ConnectSession" ADD CONSTRAINT "ConnectSession_actorUserId_User_id_fkey" FOREIGN KEY ("actorUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;--> statement-breakpoint +ALTER TABLE "ConnectSession" ADD CONSTRAINT "ConnectSession_actorTokenId_WorkspaceApiToken_id_fkey" FOREIGN KEY ("actorTokenId") REFERENCES "WorkspaceApiToken"("id") ON DELETE SET NULL ON UPDATE CASCADE;--> statement-breakpoint +ALTER TABLE "Connection" ADD CONSTRAINT "Connection_workspaceId_Workspace_id_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE CASCADE ON UPDATE CASCADE;--> statement-breakpoint +ALTER TABLE "Connection" ADD CONSTRAINT "Connection_inboxId_Inbox_id_fkey" FOREIGN KEY ("inboxId") REFERENCES "Inbox"("id") ON DELETE CASCADE ON UPDATE CASCADE;--> statement-breakpoint +ALTER TABLE "Connection" ADD CONSTRAINT "Connection_integrationId_Integration_id_fkey" FOREIGN KEY ("integrationId") REFERENCES "Integration"("id") ON DELETE SET NULL ON UPDATE CASCADE;--> statement-breakpoint +ALTER TABLE "Connection" ADD CONSTRAINT "Connection_createdBy_User_id_fkey" FOREIGN KEY ("createdBy") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; \ No newline at end of file diff --git a/packages/database/drizzle/20260914170910_add_connection_tables/snapshot.json b/packages/database/drizzle/20260914170910_add_connection_tables/snapshot.json new file mode 100644 index 0000000000..e90a2d6033 --- /dev/null +++ b/packages/database/drizzle/20260914170910_add_connection_tables/snapshot.json @@ -0,0 +1,45114 @@ +{ + "version": "8", + "dialect": "postgres", + "id": "73f0864a-d403-46ca-8059-127caa1e3ca2", + "prevIds": [ + "38cbc761-a0fe-498b-9731-e73354cc42a8" + ], + "ddl": [ + { + "values": [ + "pending", + "sent", + "failed", + "skipped_no_scope", + "skipped_region" + ], + "name": "adsConversionCapiStatus", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "automatic", + "rule", + "trigger" + ], + "name": "adsConversionEventSource", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "whatsapp", + "facebook", + "messenger", + "instagram" + ], + "name": "adsConversionChannel", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "lead", + "purchase" + ], + "name": "adsConversionEventType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "pending", + "success", + "error", + "processing" + ], + "name": "aiConversationEmbeddingStatus", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "pending", + "processing", + "success", + "error" + ], + "name": "aiConversationSourceStatus", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "document", + "image", + "url", + "web_search" + ], + "name": "aiConversationSourceType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "pending", + "success", + "error", + "processing" + ], + "name": "aiEmbeddingStatus", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "automated_response", + "ai_agent", + "flow", + "none" + ], + "name": "analyticsBotResponseType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "success", + "fallback" + ], + "name": "analyticsBotResult", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "flow", + "agent", + "fallback" + ], + "name": "analyticsBotRouteType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "message:sent", + "message:delivered", + "message:seen", + "message:failed", + "flow:clicked" + ], + "name": "analyticsBroadcastEventType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "contact_created", + "contact_deleted", + "contact_blocked" + ], + "name": "analyticsContactEventType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "bot", + "human" + ], + "name": "analyticsContactSenderType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "conversation_created", + "conversation_assigned", + "conversation_unassigned", + "conversation_transferred_to_human", + "conversation_transferred_to_bot", + "conversation_followed", + "conversation_unfollowed", + "conversation_archived", + "conversation_unarchived" + ], + "name": "analyticsConversationEventType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "message_human_sent", + "message_bot_sent" + ], + "name": "analyticsMessageEventType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "processing", + "ingested", + "failed" + ], + "name": "analyticsStatus", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "pending", + "synced", + "failed" + ], + "name": "appointmentExternalSyncStatus", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "inPerson", + "phoneCall", + "onlineMeeting" + ], + "name": "appointmentLocationTypeSnapshot", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "scheduled", + "cancelled" + ], + "name": "appointmentStatus", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "inPerson", + "phoneCall", + "onlineMeeting" + ], + "name": "appointmentLocationType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "rollingDays", + "dateRange", + "specificDay", + "anyFutureDate" + ], + "name": "appointmentScheduleWindowType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "minutes", + "hours", + "days" + ], + "name": "appointmentReminderTimingUnit", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "pending", + "sent", + "cancelled", + "failed" + ], + "name": "appointmentReminderDispatchStatus", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "image", + "video", + "audio", + "gif", + "file" + ], + "name": "fileType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "inbound", + "outbound" + ], + "name": "automatedResponseType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "defaultReply" + ], + "name": "automationThrottleType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "now", + "future" + ], + "name": "broadcastScheduleType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "scheduled", + "sent", + "sending", + "cancelled", + "draft", + "failed" + ], + "name": "broadcastStatus", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "whatsapp", + "messenger", + "instagram" + ], + "name": "coexistChannel", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "contacts", + "messages" + ], + "name": "coexistMessengerSyncPhase", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "init", + "running", + "succeeded", + "failed", + "partial", + "waiting" + ], + "name": "coexistRunStatus", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "coexist", + "contact_scan" + ], + "name": "coexistRunType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "male", + "female", + "unknown" + ], + "name": "gender", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "text", + "location", + "refLink", + "image", + "video", + "audio", + "gif", + "file" + ], + "name": "lastUserInputType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "pending", + "scheduled", + "completed", + "failed", + "canceled" + ], + "name": "ContactOnSmartDelayStatus", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "waitNode", + "followUp" + ], + "name": "ContactOnSmartDelayType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "active", + "archived" + ], + "name": "couponTopicStatus", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "shortText", + "email", + "phoneNumber", + "number", + "date", + "datetime", + "boolean", + "longText" + ], + "name": "customFieldType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "messenger", + "instagram", + "instagramFacebook" + ], + "name": "fbCommentAutomationType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "sent", + "failed" + ], + "name": "commentAutomationEventStatus", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "public", + "private" + ], + "name": "commentAutomationReplyChannel", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "AIAgent", + "text", + "flow", + "none" + ], + "name": "commentAutomationReplyType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "outsideSchedule", + "postNotMatched", + "commentIsReply", + "keywordsNotMatched", + "contactNotNew", + "alreadyRepliedOnPost", + "engagedOnOtherPost" + ], + "name": "commentAutomationMissReason", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "import", + "generic", + "export" + ], + "name": "fileContextType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "pending", + "uploaded", + "failed" + ], + "name": "fileStatus", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "tag", + "flow", + "customField", + "automatedResponse", + "trigger", + "webhook", + "sequence", + "emailTopic", + "fbComment", + "igComment", + "igStory", + "outboundAutomatedResponse" + ], + "name": "folderType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "instagram", + "instagramFacebook" + ], + "name": "igStoryAutomationType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "csv", + "xlsx", + "xls", + "json" + ], + "name": "importFormat", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "pending", + "processing", + "completed", + "failed" + ], + "name": "importStatus", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "contacts", + "coupons", + "products", + "flow" + ], + "name": "importType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "oauth", + "fbe" + ], + "name": "metaCatalogAuthMode", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "active", + "invalid" + ], + "name": "metaCatalogConnectionStatus", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "idle", + "queued", + "running", + "succeeded", + "partial", + "failed" + ], + "name": "metaCatalogImportStatus", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "pending_verification", + "registered", + "failed" + ], + "name": "whatsappRegistrationStatus", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "text", + "location", + "refLink" + ], + "name": "contentType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "message", + "comment" + ], + "name": "messageKind", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "incoming", + "outgoing", + "activity" + ], + "name": "messageType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "bot", + "contact", + "system", + "user", + "api" + ], + "name": "senderType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "pending", + "processing", + "completed", + "failed" + ], + "name": "MessageCleanupStatus", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "whatsapp", + "messenger", + "instagram" + ], + "name": "messagingAdChannel", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "pending", + "campaignCreated", + "adSetCreated", + "creativeCreated", + "adCreated", + "failed" + ], + "name": "messagingAdCreateState", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "draft", + "publishing", + "published", + "pausing", + "paused", + "deleting", + "deleted", + "publishFailed" + ], + "name": "messagingAdPublishState", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "push", + "import" + ], + "name": "metaCatalogItemDirection", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "all", + "category", + "selected" + ], + "name": "metaCatalogSyncScope", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "queued", + "running", + "succeeded", + "partial", + "failed" + ], + "name": "metaCatalogSyncStatus", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "luckyWheel", + "jackpot", + "gashapon", + "drawLots", + "scratchOff" + ], + "name": "minigameType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "dont_track", + "track" + ], + "name": "inventoryPolicy", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "text", + "number", + "email", + "phone", + "multipleChoice", + "date", + "datetime", + "image", + "file", + "location", + "websiteLink" + ], + "name": "questionnaireQuestionType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "inProgress", + "completed", + "cancelled", + "failed", + "timeout" + ], + "name": "questionnaireSubmissionStatus", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "refLink", + "qrCode" + ], + "name": "ReflinkType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "me" + ], + "name": "SystemFieldType", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "pending", + "installing", + "completed", + "partial", + "failed" + ], + "name": "templateInstallationStatus", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "flows", + "products", + "aiFunctions", + "aiAgents", + "calendars", + "webchats", + "keywords", + "entryPointLinks", + "triggers", + "fbCommentAutomations", + "settings", + "customFields", + "tags", + "productCategories" + ], + "name": "templateResourceCategory", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "ios", + "android" + ], + "name": "devicePlatform", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "allTime", + "oncePerHour", + "oncePerDay" + ], + "name": "defaultReplyFrequency", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "full", + "read_only" + ], + "name": "WorkspaceApiTokenPermission", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "owner", + "agent" + ], + "name": "workspaceMemberRole", + "entityType": "enums", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "MessageShard", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "ShardTimeRange", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "AdsConversionEvent", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "AdsConversionRule", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "AIAgent", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "AIConversationEmbedding", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "AIConversationSource", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "AIEmbedding", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "AIFile", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "AIFunction", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "AIMCPServer", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "AnalyticsBotMessageEvent", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "AnalyticsBroadcastEvent", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "AnalyticsContactEvent", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "AnalyticsConversationEvent", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "AnalyticsFlowNodeEvent", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "AnalyticsMessageEvent", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "AnalyticsSequenceEvent", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "AnalyticsEmailTopic", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "AnalyticsManifestStatus", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Appointment", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "AppointmentCalendar", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "AppointmentCalendarAvailability", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "AppointmentCalendarReminder", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "AppointmentReminderDispatch", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Attachment", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Account", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Invitation", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Jwk", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Session", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "User", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Verification", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "AutomatedResponse", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "AutomationThrottle", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "BotField", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Broadcast", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "BroadcastTarget", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "CoexistSyncRun", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "ConnectSession", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Connection", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Contact", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "ContactActiveHourly", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "ContactActiveMonthly", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "ContactCustomField", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "ContactInbox", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "ContactNote", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "ContactOnBroadcast", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "ContactOnSequence", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "ContactOnSmartDelay", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "ContactToTag", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "ContactToTagChannel", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Conversation", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "ConversationParticipant", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Coupon", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "CouponTopic", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "CustomField", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "DynamicImage", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "EmailTopic", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "AuditLog", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "CustomDomain", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Tenant", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "TenantHelpItem", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "UserQuota", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "WorkspaceUsage", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "ErrorLog", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "ExternalWebhook", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "FacebookLeadAdsAutomation", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "FacebookLeadAdsLead", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "FBCommentAutomation", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "FBCommentAutomationEvent", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "FBCommentAutomationMiss", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "FBCommentAutomationReply", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "File", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Flow", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "FlowAnalyticsSession", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "FlowNodeStat", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "FlowRun", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "FlowVersion", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Folder", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IgStoryAutomation", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Import", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Inbox", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "InboxContactStat", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "InboxTeam", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "InboxTeamMember", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationActiveCampaign", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationApi", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Integration", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationClaude", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationDeepseek", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationDrip", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationFacebookAds", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationGemini", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationGetResponse", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationGoogleCalendar", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationGoogleSheet", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationInstagram", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationKlaviyo", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationMailchimp", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationMailerLite", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationMessenger", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationMetaCatalog", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationMoosend", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationOpenai", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationOpenaiCompatible", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationOpenrouter", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationOutlookCalendar", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationSendGrid", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationSmtp", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationTelegram", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationTiktok", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationWebchat", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationWhatsapp", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "IntegrationZalo", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "MagicLink", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "MagicLinkStat", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "MediaLibraryFile", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "MediaLibraryFolder", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Message", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "MessageCleanup", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "MessagingAdOperation", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "MessagingAdsConnection", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "MessengerMessageTemplate", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "MetaCapiEvent", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "MetaCatalogItem", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "MetaCatalogSyncRun", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Minigame", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "MinigameContact", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "MinigamePlay", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "PlatformCredential", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Product", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "ProductAddon", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "ProductCategory", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "ProductVariant", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "ProductVariantOption", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "QuestionnaireAnswer", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Questionnaire", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "QuestionnaireQuestion", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "QuestionnaireSubmission", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "RefLinkStat", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Reflink", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "SavedReply", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Sequence", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "SequenceDispatch", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "SequenceStep", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Spreadsheet", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "SystemField", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Tag", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "TagChannel", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Template", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "TemplateInstallation", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "TemplateInstalledResource", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Trigger", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Condition", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "TriggerContactHistory", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "TriggerExecution", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "TriggerStat", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "UserDeviceToken", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "UserPersistentMenu", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Webhook", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "WebhookExecution", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "WhatsappBusinessAccount", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "WhatsappCoexistStaging", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "WhatsappFlow", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "WhatsappMessageTemplate", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "WhatsappSignupSession", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "Workspace", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "WorkspaceApiToken", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "WorkspaceMac", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "WorkspaceMember", + "entityType": "tables", + "schema": "public" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "MessageShard" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "MessageShard" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "MessageShard" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "MessageShard" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "host", + "entityType": "columns", + "schema": "public", + "table": "MessageShard" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "5432", + "generated": null, + "identity": null, + "name": "port", + "entityType": "columns", + "schema": "public", + "table": "MessageShard" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "database", + "entityType": "columns", + "schema": "public", + "table": "MessageShard" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user", + "entityType": "columns", + "schema": "public", + "table": "MessageShard" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "credentialRef", + "entityType": "columns", + "schema": "public", + "table": "MessageShard" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "'disable'", + "generated": null, + "identity": null, + "name": "sslMode", + "entityType": "columns", + "schema": "public", + "table": "MessageShard" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isActive", + "entityType": "columns", + "schema": "public", + "table": "MessageShard" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isMain", + "entityType": "columns", + "schema": "public", + "table": "MessageShard" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "shardKey", + "entityType": "columns", + "schema": "public", + "table": "MessageShard" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "readHost", + "entityType": "columns", + "schema": "public", + "table": "MessageShard" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "readPort", + "entityType": "columns", + "schema": "public", + "table": "MessageShard" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "ShardTimeRange" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "ShardTimeRange" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "ShardTimeRange" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "shardId", + "entityType": "columns", + "schema": "public", + "table": "ShardTimeRange" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "startTime", + "entityType": "columns", + "schema": "public", + "table": "ShardTimeRange" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "endTime", + "entityType": "columns", + "schema": "public", + "table": "ShardTimeRange" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "type": "adsConversionChannel", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'whatsapp'", + "generated": null, + "identity": null, + "name": "channel", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationWhatsappId", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationMessengerId", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationInstagramId", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "wabaId", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "type": "adsConversionEventSource", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "source", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "type": "adsConversionEventType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "eventType", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ctwaClid", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "adId", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactInboxId", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "currency", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "orderId", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contents", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "occurredAt", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceEventId", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "type": "adsConversionCapiStatus", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "capiStatus", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "capiSentAt", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionRule" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionRule" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionRule" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionRule" + }, + { + "type": "adsConversionChannel", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "channel", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionRule" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationWhatsappId", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionRule" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationFacebookAdsId", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionRule" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationMessengerId", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionRule" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationInstagramId", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionRule" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "adAccountId", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionRule" + }, + { + "type": "adsConversionEventType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "eventType", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionRule" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "trigger", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionRule" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "markAs", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionRule" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "enabled", + "entityType": "columns", + "schema": "public", + "table": "AdsConversionRule" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "AIAgent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "AIAgent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "AIAgent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "AIAgent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "AIAgent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "prompt", + "entityType": "columns", + "schema": "public", + "table": "AIAgent" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "", + "generated": null, + "identity": null, + "name": "messages", + "entityType": "columns", + "schema": "public", + "table": "AIAgent" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isDefault", + "entityType": "columns", + "schema": "public", + "table": "AIAgent" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isRichResponse", + "entityType": "columns", + "schema": "public", + "table": "AIAgent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "", + "generated": null, + "identity": null, + "name": "tools", + "entityType": "columns", + "schema": "public", + "table": "AIAgent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "", + "generated": null, + "identity": null, + "name": "webSearchAuthorizedDomains", + "entityType": "columns", + "schema": "public", + "table": "AIAgent" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "", + "generated": null, + "identity": null, + "name": "models", + "entityType": "columns", + "schema": "public", + "table": "AIAgent" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "temperature", + "entityType": "columns", + "schema": "public", + "table": "AIAgent" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "maxOutputTokens", + "entityType": "columns", + "schema": "public", + "table": "AIAgent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "AIConversationEmbedding" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "AIConversationEmbedding" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "AIConversationEmbedding" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceId", + "entityType": "columns", + "schema": "public", + "table": "AIConversationEmbedding" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "AIConversationEmbedding" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "conversationId", + "entityType": "columns", + "schema": "public", + "table": "AIConversationEmbedding" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "chunkIndex", + "entityType": "columns", + "schema": "public", + "table": "AIConversationEmbedding" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "content", + "entityType": "columns", + "schema": "public", + "table": "AIConversationEmbedding" + }, + { + "type": "vector(1536)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "embedding", + "entityType": "columns", + "schema": "public", + "table": "AIConversationEmbedding" + }, + { + "type": "aiConversationEmbeddingStatus", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "AIConversationEmbedding" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "errorMessage", + "entityType": "columns", + "schema": "public", + "table": "AIConversationEmbedding" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "AIConversationSource" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "AIConversationSource" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "AIConversationSource" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "AIConversationSource" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "conversationId", + "entityType": "columns", + "schema": "public", + "table": "AIConversationSource" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "messageId", + "entityType": "columns", + "schema": "public", + "table": "AIConversationSource" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "attachmentId", + "entityType": "columns", + "schema": "public", + "table": "AIConversationSource" + }, + { + "type": "aiConversationSourceType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceType", + "entityType": "columns", + "schema": "public", + "table": "AIConversationSource" + }, + { + "type": "aiConversationSourceStatus", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "AIConversationSource" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceKey", + "entityType": "columns", + "schema": "public", + "table": "AIConversationSource" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contentHash", + "entityType": "columns", + "schema": "public", + "table": "AIConversationSource" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "mimeType", + "entityType": "columns", + "schema": "public", + "table": "AIConversationSource" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "AIConversationSource" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "metadata", + "entityType": "columns", + "schema": "public", + "table": "AIConversationSource" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "summary", + "entityType": "columns", + "schema": "public", + "table": "AIConversationSource" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "errorMessage", + "entityType": "columns", + "schema": "public", + "table": "AIConversationSource" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "AIEmbedding" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "AIEmbedding" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "AIEmbedding" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "content", + "entityType": "columns", + "schema": "public", + "table": "AIEmbedding" + }, + { + "type": "vector(1536)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "embedding", + "entityType": "columns", + "schema": "public", + "table": "AIEmbedding" + }, + { + "type": "aiEmbeddingStatus", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "AIEmbedding" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "AIEmbedding" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "aiFileId", + "entityType": "columns", + "schema": "public", + "table": "AIEmbedding" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "AIFile" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "AIFile" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "AIFile" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "AIFile" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "path", + "entityType": "columns", + "schema": "public", + "table": "AIFile" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "size", + "entityType": "columns", + "schema": "public", + "table": "AIFile" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "mimeType", + "entityType": "columns", + "schema": "public", + "table": "AIFile" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "AIFile" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "AIFunction" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "AIFunction" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "AIFunction" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "AIFunction" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "purpose", + "entityType": "columns", + "schema": "public", + "table": "AIFunction" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "dataCollect", + "entityType": "columns", + "schema": "public", + "table": "AIFunction" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "outputMessage", + "entityType": "columns", + "schema": "public", + "table": "AIFunction" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "triggerFlowId", + "entityType": "columns", + "schema": "public", + "table": "AIFunction" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "AIFunction" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "AIMCPServer" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "AIMCPServer" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "AIMCPServer" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "AIMCPServer" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "url", + "entityType": "columns", + "schema": "public", + "table": "AIMCPServer" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "AIMCPServer" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "availableTools", + "entityType": "columns", + "schema": "public", + "table": "AIMCPServer" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "", + "generated": null, + "identity": null, + "name": "selectedTools", + "entityType": "columns", + "schema": "public", + "table": "AIMCPServer" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "AIMCPServer" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "eventId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsBotMessageEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsBotMessageEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "messageId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsBotMessageEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "conversationId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsBotMessageEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "occurredAt", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsBotMessageEvent" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "hasResponse", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsBotMessageEvent" + }, + { + "type": "analyticsBotResponseType", + "typeSchema": "public", + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "responseType", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsBotMessageEvent" + }, + { + "type": "analyticsBotRouteType", + "typeSchema": "public", + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "routeType", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsBotMessageEvent" + }, + { + "type": "analyticsBotResult", + "typeSchema": "public", + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "result", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsBotMessageEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "aiProvider", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsBotMessageEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "channel", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsBotMessageEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "source", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsBotMessageEvent" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "metadata", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsBotMessageEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "insertedAt", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsBotMessageEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsBroadcastEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "broadcastId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsBroadcastEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactInboxId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsBroadcastEvent" + }, + { + "type": "analyticsBroadcastEventType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "eventType", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsBroadcastEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "batchId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsBroadcastEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "occurredAt", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsBroadcastEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "insertedAt", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsBroadcastEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "eventId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsContactEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsContactEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsContactEvent" + }, + { + "type": "analyticsContactEventType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "eventType", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsContactEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "occurredAt", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsContactEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "source", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsContactEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsContactEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "channel", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsContactEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "country", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsContactEvent" + }, + { + "type": "analyticsContactSenderType", + "typeSchema": "public", + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "senderType", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsContactEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "adminId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsContactEvent" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "metadata", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsContactEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "insertedAt", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsContactEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "eventId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsConversationEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsConversationEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "conversationId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsConversationEvent" + }, + { + "type": "analyticsConversationEventType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "eventType", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsConversationEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "occurredAt", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsConversationEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fromAssignee", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsConversationEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "toAssignee", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsConversationEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "channel", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsConversationEvent" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "metadata", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsConversationEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "insertedAt", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsConversationEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsFlowNodeEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "flowId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsFlowNodeEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "analyticsId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsFlowNodeEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "nodeId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsFlowNodeEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "buttonId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsFlowNodeEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactInboxId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsFlowNodeEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "eventType", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsFlowNodeEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "occurredAt", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsFlowNodeEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "insertedAt", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsFlowNodeEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "eventId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsMessageEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsMessageEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsMessageEvent" + }, + { + "type": "analyticsMessageEventType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "eventType", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsMessageEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "occurredAt", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsMessageEvent" + }, + { + "type": "analyticsContactSenderType", + "typeSchema": "public", + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "senderType", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsMessageEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "adminId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsMessageEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "channel", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsMessageEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "source", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsMessageEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsMessageEvent" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "metadata", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsMessageEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "insertedAt", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsMessageEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsSequenceEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactInboxId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsSequenceEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "eventType", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsSequenceEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sequenceId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsSequenceEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "stepId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsSequenceEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "occurredAt", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsSequenceEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "insertedAt", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsSequenceEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "topicId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "conversationId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactInboxId", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deliveredAt", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "failedAt", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "firstSeenAt", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastSeenAt", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "seenCount", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "firstClickedAt", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastClickedAt", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "clickCount", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "objectKey", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsManifestStatus" + }, + { + "type": "analyticsStatus", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsManifestStatus" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "attempts", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsManifestStatus" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ingestedAt", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsManifestStatus" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastError", + "entityType": "columns", + "schema": "public", + "table": "AnalyticsManifestStatus" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Appointment" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Appointment" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Appointment" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "Appointment" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "calendarId", + "entityType": "columns", + "schema": "public", + "table": "Appointment" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "Appointment" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "conversationId", + "entityType": "columns", + "schema": "public", + "table": "Appointment" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactInboxId", + "entityType": "columns", + "schema": "public", + "table": "Appointment" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "startAt", + "entityType": "columns", + "schema": "public", + "table": "Appointment" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "endAt", + "entityType": "columns", + "schema": "public", + "table": "Appointment" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inviteeTimezone", + "entityType": "columns", + "schema": "public", + "table": "Appointment" + }, + { + "type": "appointmentStatus", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'scheduled'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "Appointment" + }, + { + "type": "appointmentLocationTypeSnapshot", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "locationType", + "entityType": "columns", + "schema": "public", + "table": "Appointment" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "locationDetail", + "entityType": "columns", + "schema": "public", + "table": "Appointment" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "externalEventId", + "entityType": "columns", + "schema": "public", + "table": "Appointment" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "externalEventIntegrationId", + "entityType": "columns", + "schema": "public", + "table": "Appointment" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "externalEventProviderCalendarId", + "entityType": "columns", + "schema": "public", + "table": "Appointment" + }, + { + "type": "appointmentExternalSyncStatus", + "typeSchema": "public", + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "externalSyncStatus", + "entityType": "columns", + "schema": "public", + "table": "Appointment" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "cancelledAt", + "entityType": "columns", + "schema": "public", + "table": "Appointment" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deletedAt", + "entityType": "columns", + "schema": "public", + "table": "Appointment" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "active", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "timezone", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "30", + "generated": null, + "identity": null, + "name": "durationMinutes", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "bufferAfterMinutes", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "appointmentLocationType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "locationType", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "locationDetail", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "appointmentScheduleWindowType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'rollingDays'", + "generated": null, + "identity": null, + "name": "scheduleWindowType", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "scheduleWindowConfig", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "maxAppointmentsPerUser", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "dailyLimitEnabled", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "maxPerDay", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "allowGroupMeeting", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "maxPerSlot", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "confirmationMessage", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "confirmationFlowId", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "cancellationFlowId", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "externalConnectionId", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "externalEventTitleTemplate", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "externalEventDescriptionTemplate", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "externalEventAttendeesTemplate", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publicLinkSlug", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deletedAt", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendarAvailability" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendarAvailability" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendarAvailability" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "calendarId", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendarAvailability" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "weekday", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendarAvailability" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "startMinute", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendarAvailability" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "endMinute", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendarAvailability" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendarReminder" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendarReminder" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendarReminder" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "calendarId", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendarReminder" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "flowId", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendarReminder" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "timingValue", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendarReminder" + }, + { + "type": "appointmentReminderTimingUnit", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "timingUnit", + "entityType": "columns", + "schema": "public", + "table": "AppointmentCalendarReminder" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "AppointmentReminderDispatch" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "AppointmentReminderDispatch" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "AppointmentReminderDispatch" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "AppointmentReminderDispatch" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "appointmentId", + "entityType": "columns", + "schema": "public", + "table": "AppointmentReminderDispatch" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reminderConfigId", + "entityType": "columns", + "schema": "public", + "table": "AppointmentReminderDispatch" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactInboxId", + "entityType": "columns", + "schema": "public", + "table": "AppointmentReminderDispatch" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sendAt", + "entityType": "columns", + "schema": "public", + "table": "AppointmentReminderDispatch" + }, + { + "type": "appointmentReminderDispatchStatus", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "AppointmentReminderDispatch" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "jobId", + "entityType": "columns", + "schema": "public", + "table": "AppointmentReminderDispatch" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sentAt", + "entityType": "columns", + "schema": "public", + "table": "AppointmentReminderDispatch" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "cancelledAt", + "entityType": "columns", + "schema": "public", + "table": "AppointmentReminderDispatch" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "failedReason", + "entityType": "columns", + "schema": "public", + "table": "AppointmentReminderDispatch" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Attachment" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Attachment" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Attachment" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "Attachment" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "conversationId", + "entityType": "columns", + "schema": "public", + "table": "Attachment" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "messageId", + "entityType": "columns", + "schema": "public", + "table": "Attachment" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "messageCreatedAt", + "entityType": "columns", + "schema": "public", + "table": "Attachment" + }, + { + "type": "fileType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fileType", + "entityType": "columns", + "schema": "public", + "table": "Attachment" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceId", + "entityType": "columns", + "schema": "public", + "table": "Attachment" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "mimeType", + "entityType": "columns", + "schema": "public", + "table": "Attachment" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "width", + "entityType": "columns", + "schema": "public", + "table": "Attachment" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "height", + "entityType": "columns", + "schema": "public", + "table": "Attachment" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "size", + "entityType": "columns", + "schema": "public", + "table": "Attachment" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "thumbnailPath", + "entityType": "columns", + "schema": "public", + "table": "Attachment" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "originPath", + "entityType": "columns", + "schema": "public", + "table": "Attachment" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "Attachment" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Account" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Account" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "accountId", + "entityType": "columns", + "schema": "public", + "table": "Account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerId", + "entityType": "columns", + "schema": "public", + "table": "Account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "accessToken", + "entityType": "columns", + "schema": "public", + "table": "Account" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "accessTokenExpiresAt", + "entityType": "columns", + "schema": "public", + "table": "Account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refreshToken", + "entityType": "columns", + "schema": "public", + "table": "Account" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refreshTokenExpiresAt", + "entityType": "columns", + "schema": "public", + "table": "Account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "scope", + "entityType": "columns", + "schema": "public", + "table": "Account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "idToken", + "entityType": "columns", + "schema": "public", + "table": "Account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "password", + "entityType": "columns", + "schema": "public", + "table": "Account" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "Account" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "tenantId", + "entityType": "columns", + "schema": "public", + "table": "Account" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Invitation" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Invitation" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "code", + "entityType": "columns", + "schema": "public", + "table": "Invitation" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "permissions", + "entityType": "columns", + "schema": "public", + "table": "Invitation" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "Invitation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "Invitation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "invitedBy", + "entityType": "columns", + "schema": "public", + "table": "Invitation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Jwk" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Jwk" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Jwk" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publicKey", + "entityType": "columns", + "schema": "public", + "table": "Jwk" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "privateKey", + "entityType": "columns", + "schema": "public", + "table": "Jwk" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "Jwk" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Session" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Session" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Session" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "Session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "public", + "table": "Session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ipAddress", + "entityType": "columns", + "schema": "public", + "table": "Session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userAgent", + "entityType": "columns", + "schema": "public", + "table": "Session" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "Session" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "User" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "User" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "User" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "User" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "User" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "emailVerified", + "entityType": "columns", + "schema": "public", + "table": "User" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "image", + "entityType": "columns", + "schema": "public", + "table": "User" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isAnonymous", + "entityType": "columns", + "schema": "public", + "table": "User" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "mustChangePassword", + "entityType": "columns", + "schema": "public", + "table": "User" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "tenantId", + "entityType": "columns", + "schema": "public", + "table": "User" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Verification" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Verification" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Verification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "identifier", + "entityType": "columns", + "schema": "public", + "table": "Verification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "public", + "table": "Verification" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "Verification" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "AutomatedResponse" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "AutomatedResponse" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "AutomatedResponse" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "AutomatedResponse" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "folderId", + "entityType": "columns", + "schema": "public", + "table": "AutomatedResponse" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "", + "generated": null, + "identity": null, + "name": "keywords", + "entityType": "columns", + "schema": "public", + "table": "AutomatedResponse" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "AutomatedResponse" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "text", + "entityType": "columns", + "schema": "public", + "table": "AutomatedResponse" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "flowId", + "entityType": "columns", + "schema": "public", + "table": "AutomatedResponse" + }, + { + "type": "automatedResponseType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'inbound'", + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "AutomatedResponse" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "AutomationThrottle" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactInboxId", + "entityType": "columns", + "schema": "public", + "table": "AutomationThrottle" + }, + { + "type": "automationThrottleType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "throttleType", + "entityType": "columns", + "schema": "public", + "table": "AutomationThrottle" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "subjectId", + "entityType": "columns", + "schema": "public", + "table": "AutomationThrottle" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "lastTriggeredAt", + "entityType": "columns", + "schema": "public", + "table": "AutomationThrottle" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "claimId", + "entityType": "columns", + "schema": "public", + "table": "AutomationThrottle" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "BotField" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "BotField" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "BotField" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "BotField" + }, + { + "type": "customFieldType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "BotField" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "public", + "table": "BotField" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "BotField" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "folderId", + "entityType": "columns", + "schema": "public", + "table": "BotField" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "BotField" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Broadcast" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Broadcast" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Broadcast" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "Broadcast" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "Broadcast" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "flowId", + "entityType": "columns", + "schema": "public", + "table": "Broadcast" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationWhatsappId", + "entityType": "columns", + "schema": "public", + "table": "Broadcast" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationMessengerId", + "entityType": "columns", + "schema": "public", + "table": "Broadcast" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "templateId", + "entityType": "columns", + "schema": "public", + "table": "Broadcast" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "templateData", + "entityType": "columns", + "schema": "public", + "table": "Broadcast" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'channel'", + "generated": null, + "identity": null, + "name": "targetMode", + "entityType": "columns", + "schema": "public", + "table": "Broadcast" + }, + { + "type": "broadcastStatus", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "Broadcast" + }, + { + "type": "broadcastScheduleType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "schedulesType", + "entityType": "columns", + "schema": "public", + "table": "Broadcast" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "schedulesAt", + "entityType": "columns", + "schema": "public", + "table": "Broadcast" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactFilter", + "entityType": "columns", + "schema": "public", + "table": "Broadcast" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "subaction", + "entityType": "columns", + "schema": "public", + "table": "Broadcast" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "channel", + "entityType": "columns", + "schema": "public", + "table": "Broadcast" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactCount", + "entityType": "columns", + "schema": "public", + "table": "Broadcast" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "handoffCompletedAt", + "entityType": "columns", + "schema": "public", + "table": "Broadcast" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deletedAt", + "entityType": "columns", + "schema": "public", + "table": "Broadcast" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "resumeCount", + "entityType": "columns", + "schema": "public", + "table": "Broadcast" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "broadcastId", + "entityType": "columns", + "schema": "public", + "table": "BroadcastTarget" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inboxId", + "entityType": "columns", + "schema": "public", + "table": "BroadcastTarget" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "flowId", + "entityType": "columns", + "schema": "public", + "table": "BroadcastTarget" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "templateId", + "entityType": "columns", + "schema": "public", + "table": "BroadcastTarget" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "templateData", + "entityType": "columns", + "schema": "public", + "table": "BroadcastTarget" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationId", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "coexistChannel", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "channel", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "coexistRunType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'coexist'", + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "coexistRunStatus", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'init'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "triggerSource", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "startedAt", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "finishedAt", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastHeartbeatAt", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "totalScan", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "currentScan", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "currentStep", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastSyncedAt", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "currentPageNumber", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "importedContactCount", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "importedMessageCount", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "skippedCount", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "failedCount", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "attempts", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "currentError", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastPhase", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastChunkOrder", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "syncProgress", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "coexistMessengerSyncPhase", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'contacts'", + "generated": null, + "identity": null, + "name": "messengerSyncPhase", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pendingPatches", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "claimToken", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "scanFromAt", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "requestedByUserId", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "resumeCursor", + "entityType": "columns", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "ConnectSession" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "ConnectSession" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "ConnectSession" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "ConnectSession" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider", + "entityType": "columns", + "schema": "public", + "table": "ConnectSession" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "purpose", + "entityType": "columns", + "schema": "public", + "table": "ConnectSession" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "targetConnectionId", + "entityType": "columns", + "schema": "public", + "table": "ConnectSession" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "actorUserId", + "entityType": "columns", + "schema": "public", + "table": "ConnectSession" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "actorTokenId", + "entityType": "columns", + "schema": "public", + "table": "ConnectSession" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "platformOwnerId", + "entityType": "columns", + "schema": "public", + "table": "ConnectSession" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "originHost", + "entityType": "columns", + "schema": "public", + "table": "ConnectSession" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "returnUrl", + "entityType": "columns", + "schema": "public", + "table": "ConnectSession" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "stateNonceHash", + "entityType": "columns", + "schema": "public", + "table": "ConnectSession" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "ConnectSession" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'authorize'", + "generated": null, + "identity": null, + "name": "step", + "entityType": "columns", + "schema": "public", + "table": "ConnectSession" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "nextAction", + "entityType": "columns", + "schema": "public", + "table": "ConnectSession" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "encryptedAuth", + "entityType": "columns", + "schema": "public", + "table": "ConnectSession" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "", + "generated": null, + "identity": null, + "name": "targets", + "entityType": "columns", + "schema": "public", + "table": "ConnectSession" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "", + "generated": null, + "identity": null, + "name": "claimedTargetIds", + "entityType": "columns", + "schema": "public", + "table": "ConnectSession" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "", + "generated": null, + "identity": null, + "name": "resultConnectionIds", + "entityType": "columns", + "schema": "public", + "table": "ConnectSession" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "", + "generated": null, + "identity": null, + "name": "results", + "entityType": "columns", + "schema": "public", + "table": "ConnectSession" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "errorCode", + "entityType": "columns", + "schema": "public", + "table": "ConnectSession" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "ConnectSession" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "consumedAt", + "entityType": "columns", + "schema": "public", + "table": "ConnectSession" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Connection" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Connection" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Connection" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "Connection" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider", + "entityType": "columns", + "schema": "public", + "table": "Connection" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "kind", + "entityType": "columns", + "schema": "public", + "table": "Connection" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "channel", + "entityType": "columns", + "schema": "public", + "table": "Connection" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inboxId", + "entityType": "columns", + "schema": "public", + "table": "Connection" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationId", + "entityType": "columns", + "schema": "public", + "table": "Connection" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceId", + "entityType": "columns", + "schema": "public", + "table": "Connection" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "displayName", + "entityType": "columns", + "schema": "public", + "table": "Connection" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'connected'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "Connection" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "statusReason", + "entityType": "columns", + "schema": "public", + "table": "Connection" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastError", + "entityType": "columns", + "schema": "public", + "table": "Connection" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "authExpiresAt", + "entityType": "columns", + "schema": "public", + "table": "Connection" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "createdBy", + "entityType": "columns", + "schema": "public", + "table": "Connection" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "connectedAt", + "entityType": "columns", + "schema": "public", + "table": "Connection" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "disconnectedAt", + "entityType": "columns", + "schema": "public", + "table": "Connection" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Contact" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Contact" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Contact" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "avatar", + "entityType": "columns", + "schema": "public", + "table": "Contact" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "phoneNumber", + "entityType": "columns", + "schema": "public", + "table": "Contact" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "Contact" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "emailVerified", + "entityType": "columns", + "schema": "public", + "table": "Contact" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "emailOptIn", + "entityType": "columns", + "schema": "public", + "table": "Contact" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "firstName", + "entityType": "columns", + "schema": "public", + "table": "Contact" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastName", + "entityType": "columns", + "schema": "public", + "table": "Contact" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": { + "as": "CASE\n WHEN \"firstName\" IS NULL AND \"lastName\" IS NULL THEN NULL\n WHEN \"firstName\" IS NULL THEN \"lastName\"\n WHEN \"lastName\" IS NULL THEN \"firstName\"\n ELSE \"firstName\" || ' ' || \"lastName\"\n END", + "type": "stored" + }, + "identity": null, + "name": "fullName", + "entityType": "columns", + "schema": "public", + "table": "Contact" + }, + { + "type": "gender", + "typeSchema": "public", + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "gender", + "entityType": "columns", + "schema": "public", + "table": "Contact" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastReadAt", + "entityType": "columns", + "schema": "public", + "table": "Contact" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ref", + "entityType": "columns", + "schema": "public", + "table": "Contact" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "country", + "entityType": "columns", + "schema": "public", + "table": "Contact" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "state", + "entityType": "columns", + "schema": "public", + "table": "Contact" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "city", + "entityType": "columns", + "schema": "public", + "table": "Contact" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "location", + "entityType": "columns", + "schema": "public", + "table": "Contact" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "locale", + "entityType": "columns", + "schema": "public", + "table": "Contact" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "timezone", + "entityType": "columns", + "schema": "public", + "table": "Contact" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "subscribedAt", + "entityType": "columns", + "schema": "public", + "table": "Contact" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "broadcastSubscribedAt", + "entityType": "columns", + "schema": "public", + "table": "Contact" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "blockedAt", + "entityType": "columns", + "schema": "public", + "table": "Contact" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "Contact" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "ContactActiveHourly" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "ContactActiveHourly" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactInboxId", + "entityType": "columns", + "schema": "public", + "table": "ContactActiveHourly" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hourBucket", + "entityType": "columns", + "schema": "public", + "table": "ContactActiveHourly" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inboxId", + "entityType": "columns", + "schema": "public", + "table": "ContactActiveHourly" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "ContactActiveMonthly" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "ContactActiveMonthly" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactInboxId", + "entityType": "columns", + "schema": "public", + "table": "ContactActiveMonthly" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "periodStart", + "entityType": "columns", + "schema": "public", + "table": "ContactActiveMonthly" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inboxId", + "entityType": "columns", + "schema": "public", + "table": "ContactActiveMonthly" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceMacId", + "entityType": "columns", + "schema": "public", + "table": "ContactActiveMonthly" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "ContactCustomField" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "ContactCustomField" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "ContactCustomField" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "public", + "table": "ContactCustomField" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "ContactCustomField" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "customFieldId", + "entityType": "columns", + "schema": "public", + "table": "ContactCustomField" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "originalContactId", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inboxId", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "channel", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "source", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceId", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "language", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "personaId", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactLastReadAt", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "firstInteractionAt", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastMessageAt", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastIncomingMessageAt", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastOutboundMessageAt", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "referral", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastCommentMessageId", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastCommentMessageAt", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "consecutiveFailedReply", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastInputFailure", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastErrorLog", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastBtnTitle", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastUserInput", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "lastUserInputType", + "typeSchema": "public", + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastUserInputType", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "webchatParentUrl", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceUserId", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceUsername", + "entityType": "columns", + "schema": "public", + "table": "ContactInbox" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "ContactNote" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "ContactNote" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "ContactNote" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "text", + "entityType": "columns", + "schema": "public", + "table": "ContactNote" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "ContactNote" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "createdById", + "entityType": "columns", + "schema": "public", + "table": "ContactNote" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "broadcastId", + "entityType": "columns", + "schema": "public", + "table": "ContactOnBroadcast" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "ContactOnBroadcast" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactInboxId", + "entityType": "columns", + "schema": "public", + "table": "ContactOnBroadcast" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "conversationId", + "entityType": "columns", + "schema": "public", + "table": "ContactOnBroadcast" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "sent", + "entityType": "columns", + "schema": "public", + "table": "ContactOnBroadcast" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "seenAt", + "entityType": "columns", + "schema": "public", + "table": "ContactOnBroadcast" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deliveredAt", + "entityType": "columns", + "schema": "public", + "table": "ContactOnBroadcast" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "clickedAt", + "entityType": "columns", + "schema": "public", + "table": "ContactOnBroadcast" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "failedAt", + "entityType": "columns", + "schema": "public", + "table": "ContactOnBroadcast" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "errorContent", + "entityType": "columns", + "schema": "public", + "table": "ContactOnBroadcast" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": { + "as": "case when \"seenAt\" is null then false when \"deliveredAt\" is null then false else \"seenAt\" >= \"deliveredAt\" end", + "type": "stored" + }, + "identity": null, + "name": "isRead", + "entityType": "columns", + "schema": "public", + "table": "ContactOnBroadcast" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "enrolledAt", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "completedAt", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "currentStep", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "nextRunAt", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastStepId", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "nextStepId", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lockedAt", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lockOwner", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastError", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sequenceId", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSmartDelay" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSmartDelay" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "flowId", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSmartDelay" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "flowVersionId", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSmartDelay" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactInboxId", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSmartDelay" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "appointmentId", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSmartDelay" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "conversationId", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSmartDelay" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "nodeId", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSmartDelay" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "stepId", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSmartDelay" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "metadata", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSmartDelay" + }, + { + "type": "ContactOnSmartDelayType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSmartDelay" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSmartDelay" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "triggerAt", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSmartDelay" + }, + { + "type": "ContactOnSmartDelayStatus", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "ContactOnSmartDelay" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "ContactToTag" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tagId", + "entityType": "columns", + "schema": "public", + "table": "ContactToTag" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tagId", + "entityType": "columns", + "schema": "public", + "table": "ContactToTagChannel" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tagChannelId", + "entityType": "columns", + "schema": "public", + "table": "ContactToTagChannel" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactInboxId", + "entityType": "columns", + "schema": "public", + "table": "ContactToTagChannel" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "ContactToTagChannel" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Conversation" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Conversation" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Conversation" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "botEnabled", + "entityType": "columns", + "schema": "public", + "table": "Conversation" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "botResumeAt", + "entityType": "columns", + "schema": "public", + "table": "Conversation" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "archivedAt", + "entityType": "columns", + "schema": "public", + "table": "Conversation" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "additionalAttributes", + "entityType": "columns", + "schema": "public", + "table": "Conversation" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactLastReadAt", + "entityType": "columns", + "schema": "public", + "table": "Conversation" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "agentLastReadAt", + "entityType": "columns", + "schema": "public", + "table": "Conversation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "aiContextLastMessageId", + "entityType": "columns", + "schema": "public", + "table": "Conversation" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastActivityAt", + "entityType": "columns", + "schema": "public", + "table": "Conversation" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "followed", + "entityType": "columns", + "schema": "public", + "table": "Conversation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "assignedUserId", + "entityType": "columns", + "schema": "public", + "table": "Conversation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "assignedInboxTeamId", + "entityType": "columns", + "schema": "public", + "table": "Conversation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "Conversation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "Conversation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceId", + "entityType": "columns", + "schema": "public", + "table": "Conversation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastStep", + "entityType": "columns", + "schema": "public", + "table": "Conversation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "currentStep", + "entityType": "columns", + "schema": "public", + "table": "Conversation" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "adminRepliedAt", + "entityType": "columns", + "schema": "public", + "table": "Conversation" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactRepliedAt", + "entityType": "columns", + "schema": "public", + "table": "Conversation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "ConversationParticipant" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "ConversationParticipant" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "ConversationParticipant" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "ConversationParticipant" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "conversationId", + "entityType": "columns", + "schema": "public", + "table": "ConversationParticipant" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "ConversationParticipant" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Coupon" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Coupon" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Coupon" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "Coupon" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "topicId", + "entityType": "columns", + "schema": "public", + "table": "Coupon" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "code", + "entityType": "columns", + "schema": "public", + "table": "Coupon" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "issuedContactId", + "entityType": "columns", + "schema": "public", + "table": "Coupon" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "issuedAt", + "entityType": "columns", + "schema": "public", + "table": "Coupon" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "usedAt", + "entityType": "columns", + "schema": "public", + "table": "Coupon" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "CouponTopic" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "CouponTopic" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "CouponTopic" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "CouponTopic" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "CouponTopic" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "CouponTopic" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "CouponTopic" + }, + { + "type": "couponTopicStatus", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'active'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "CouponTopic" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deletedAt", + "entityType": "columns", + "schema": "public", + "table": "CouponTopic" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "hasEverHadCoupon", + "entityType": "columns", + "schema": "public", + "table": "CouponTopic" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "createdById", + "entityType": "columns", + "schema": "public", + "table": "CouponTopic" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "CustomField" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "CustomField" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "CustomField" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "CustomField" + }, + { + "type": "customFieldType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "CustomField" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "CustomField" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "folderId", + "entityType": "columns", + "schema": "public", + "table": "CustomField" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "showInInbox", + "entityType": "columns", + "schema": "public", + "table": "CustomField" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "CustomField" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "DynamicImage" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "DynamicImage" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "DynamicImage" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "DynamicImage" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "DynamicImage" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "customFieldId", + "entityType": "columns", + "schema": "public", + "table": "DynamicImage" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "data", + "entityType": "columns", + "schema": "public", + "table": "DynamicImage" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "backgroundUrl", + "entityType": "columns", + "schema": "public", + "table": "DynamicImage" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "enabled", + "entityType": "columns", + "schema": "public", + "table": "DynamicImage" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "EmailTopic" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "EmailTopic" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "EmailTopic" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "EmailTopic" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "EmailTopic" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "folderId", + "entityType": "columns", + "schema": "public", + "table": "EmailTopic" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "sendsTotal", + "entityType": "columns", + "schema": "public", + "table": "EmailTopic" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "deliveredsTotal", + "entityType": "columns", + "schema": "public", + "table": "EmailTopic" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "seensTotal", + "entityType": "columns", + "schema": "public", + "table": "EmailTopic" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "clicksTotal", + "entityType": "columns", + "schema": "public", + "table": "EmailTopic" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "AuditLog" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "AuditLog" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "AuditLog" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "action", + "entityType": "columns", + "schema": "public", + "table": "AuditLog" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "detail", + "entityType": "columns", + "schema": "public", + "table": "AuditLog" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ipAddress", + "entityType": "columns", + "schema": "public", + "table": "AuditLog" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userAgent", + "entityType": "columns", + "schema": "public", + "table": "AuditLog" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "source", + "entityType": "columns", + "schema": "public", + "table": "AuditLog" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "AuditLog" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "AuditLog" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "CustomDomain" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "CustomDomain" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "CustomDomain" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tenantId", + "entityType": "columns", + "schema": "public", + "table": "CustomDomain" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "domain", + "entityType": "columns", + "schema": "public", + "table": "CustomDomain" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "CustomDomain" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "verifiedAt", + "entityType": "columns", + "schema": "public", + "table": "CustomDomain" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "cfHostnameId", + "entityType": "columns", + "schema": "public", + "table": "CustomDomain" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "cfOwnershipValue", + "entityType": "columns", + "schema": "public", + "table": "CustomDomain" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "cfAcmeValue", + "entityType": "columns", + "schema": "public", + "table": "CustomDomain" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Tenant" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Tenant" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Tenant" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ownerId", + "entityType": "columns", + "schema": "public", + "table": "Tenant" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'active'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "Tenant" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "disabledReason", + "entityType": "columns", + "schema": "public", + "table": "Tenant" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "brandName", + "entityType": "columns", + "schema": "public", + "table": "Tenant" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "logoLightPath", + "entityType": "columns", + "schema": "public", + "table": "Tenant" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "logoDarkPath", + "entityType": "columns", + "schema": "public", + "table": "Tenant" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "faviconPath", + "entityType": "columns", + "schema": "public", + "table": "Tenant" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "customCss", + "entityType": "columns", + "schema": "public", + "table": "Tenant" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "customJs", + "entityType": "columns", + "schema": "public", + "table": "Tenant" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "theme", + "entityType": "columns", + "schema": "public", + "table": "Tenant" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "storageUrl", + "entityType": "columns", + "schema": "public", + "table": "Tenant" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "policyUrl", + "entityType": "columns", + "schema": "public", + "table": "Tenant" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "termsOfServiceUrl", + "entityType": "columns", + "schema": "public", + "table": "Tenant" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "signupEmailTemplate", + "entityType": "columns", + "schema": "public", + "table": "Tenant" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "forgotPasswordEmailTemplate", + "entityType": "columns", + "schema": "public", + "table": "Tenant" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "magicLinkEmailTemplate", + "entityType": "columns", + "schema": "public", + "table": "Tenant" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "accountCredentialsEmailTemplate", + "entityType": "columns", + "schema": "public", + "table": "Tenant" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenChannels", + "entityType": "columns", + "schema": "public", + "table": "Tenant" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "TenantHelpItem" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "TenantHelpItem" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "TenantHelpItem" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tenantId", + "entityType": "columns", + "schema": "public", + "table": "TenantHelpItem" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "TenantHelpItem" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "url", + "entityType": "columns", + "schema": "public", + "table": "TenantHelpItem" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "icon", + "entityType": "columns", + "schema": "public", + "table": "TenantHelpItem" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "TenantHelpItem" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactsLimit", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "contactsUsed", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspacesLimit", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "workspacesUsed", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "channelsLimit", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "channelsUsed", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "teamMembersLimit", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "teamMembersUsed", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "macLimit", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "macUsed", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "botMessagesLimit", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "botMessagesUsed", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "monthlyBotMessagesLimit", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "monthlyBotMessagesUsed", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "monthlyBotMessagesPeriodStart", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "botMessagesTopUpGranted", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "whiteLabel", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "ssoSaml", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "saasMode", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "planName", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "planStatus", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "selectedTrialPlanId", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "periodStart", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "periodEnd", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "channelsTornDownAt", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "syncedAt", + "entityType": "columns", + "schema": "public", + "table": "UserQuota" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceUsage" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceUsage" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceUsage" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceUsage" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "contactsUsed", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceUsage" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "channelsUsed", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceUsage" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "teamMembersUsed", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceUsage" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "botMessagesUsed", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceUsage" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "macUsed", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceUsage" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "syncedAt", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceUsage" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "ErrorLog" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "ErrorLog" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "ErrorLog" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "action", + "entityType": "columns", + "schema": "public", + "table": "ErrorLog" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "detail", + "entityType": "columns", + "schema": "public", + "table": "ErrorLog" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "httpCode", + "entityType": "columns", + "schema": "public", + "table": "ErrorLog" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "ErrorLog" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "ErrorLog" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceId", + "entityType": "columns", + "schema": "public", + "table": "ErrorLog" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "stackTrace", + "entityType": "columns", + "schema": "public", + "table": "ErrorLog" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "ExternalWebhook" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "ExternalWebhook" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "ExternalWebhook" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'make'", + "generated": null, + "identity": null, + "name": "provider", + "entityType": "columns", + "schema": "public", + "table": "ExternalWebhook" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "event", + "entityType": "columns", + "schema": "public", + "table": "ExternalWebhook" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "url", + "entityType": "columns", + "schema": "public", + "table": "ExternalWebhook" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "ExternalWebhook" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "FacebookLeadAdsAutomation" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "FacebookLeadAdsAutomation" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "FacebookLeadAdsAutomation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "FacebookLeadAdsAutomation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "FacebookLeadAdsAutomation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pageId", + "entityType": "columns", + "schema": "public", + "table": "FacebookLeadAdsAutomation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pageName", + "entityType": "columns", + "schema": "public", + "table": "FacebookLeadAdsAutomation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "formId", + "entityType": "columns", + "schema": "public", + "table": "FacebookLeadAdsAutomation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "formName", + "entityType": "columns", + "schema": "public", + "table": "FacebookLeadAdsAutomation" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "fieldMapping", + "entityType": "columns", + "schema": "public", + "table": "FacebookLeadAdsAutomation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "flowId", + "entityType": "columns", + "schema": "public", + "table": "FacebookLeadAdsAutomation" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "leadsHandledCount", + "entityType": "columns", + "schema": "public", + "table": "FacebookLeadAdsAutomation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "FacebookLeadAdsLead" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "FacebookLeadAdsLead" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "FacebookLeadAdsLead" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "automationId", + "entityType": "columns", + "schema": "public", + "table": "FacebookLeadAdsLead" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "leadgenId", + "entityType": "columns", + "schema": "public", + "table": "FacebookLeadAdsLead" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "FacebookLeadAdsLead" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "folderId", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "type": "fbCommentAutomationType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'messenger'", + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "isActive", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "startTime", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "endTime", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "repliesCount", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "sentCount", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "deliveredCount", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "seenCount", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "clickedCount", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "failedCount", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "missedCount", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{\"type\":\"all\",\"value\":[]}'", + "generated": null, + "identity": null, + "name": "post", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{\"type\":\"text\",\"value\":\"\"}'", + "generated": null, + "identity": null, + "name": "privateReply", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{\"type\":\"none\",\"value\":null}'", + "generated": null, + "identity": null, + "name": "publicReply", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{\"type\":\"all\",\"value\":[]}'", + "generated": null, + "identity": null, + "name": "includeKeywords", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "ARRAY[]", + "generated": null, + "identity": null, + "name": "excludeKeywords", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{\"replyToNewContactsOnly\":false,\"replyOncePerUserPerPost\":false,\"likeUserComment\":false,\"replyToUsersWhoCommentedOnOtherPosts\":true,\"ignoreCommentReplies\":true,\"trackUserTags\":false}'", + "generated": null, + "identity": null, + "name": "options", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{\"all\":false,\"hasPhoneNumber\":false,\"hasImage\":false,\"hasVideo\":false,\"hasLink\":false,\"hasKeywords\":false,\"keywords\":[],\"showCommentsAfter\":\"none\"}'", + "generated": null, + "identity": null, + "name": "hideComments", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{\"type\":\"immediately\",\"value\":0}'", + "generated": null, + "identity": null, + "name": "replyAfter", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "automationId", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactInboxId", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "postId", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "commentId", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "commentText", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "type": "commentAutomationReplyChannel", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "replyChannel", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "type": "commentAutomationReplyType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "replyType", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "replyText", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "type": "commentAutomationEventStatus", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deliveredAt", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "seenAt", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "clickedAt", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "failedAt", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "errorDetail", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "httpCode", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "occurredAt", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationMiss" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationMiss" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationMiss" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationMiss" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "automationId", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationMiss" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationMiss" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactInboxId", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationMiss" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "postId", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationMiss" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "commentId", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationMiss" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "commentText", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationMiss" + }, + { + "type": "commentAutomationMissReason", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reason", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationMiss" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "occurredAt", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationMiss" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationReply" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationReply" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationReply" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "automationId", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationReply" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationReply" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "postId", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationReply" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "FBCommentAutomationReply" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "File" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "File" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "File" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "File" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "File" + }, + { + "type": "fileContextType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contextType", + "entityType": "columns", + "schema": "public", + "table": "File" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "subType", + "entityType": "columns", + "schema": "public", + "table": "File" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "path", + "entityType": "columns", + "schema": "public", + "table": "File" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fileName", + "entityType": "columns", + "schema": "public", + "table": "File" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "mimeType", + "entityType": "columns", + "schema": "public", + "table": "File" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fileSize", + "entityType": "columns", + "schema": "public", + "table": "File" + }, + { + "type": "fileStatus", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "File" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "meta", + "entityType": "columns", + "schema": "public", + "table": "File" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "uploadedAt", + "entityType": "columns", + "schema": "public", + "table": "File" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Flow" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Flow" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Flow" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "Flow" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "active", + "entityType": "columns", + "schema": "public", + "table": "Flow" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "enableInInbox", + "entityType": "columns", + "schema": "public", + "table": "Flow" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "currentVersionId", + "entityType": "columns", + "schema": "public", + "table": "Flow" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "draftVersionId", + "entityType": "columns", + "schema": "public", + "table": "Flow" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "Flow" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "folderId", + "entityType": "columns", + "schema": "public", + "table": "Flow" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "FlowAnalyticsSession" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "FlowAnalyticsSession" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "FlowAnalyticsSession" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "flowId", + "entityType": "columns", + "schema": "public", + "table": "FlowAnalyticsSession" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "FlowAnalyticsSession" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deletedAt", + "entityType": "columns", + "schema": "public", + "table": "FlowAnalyticsSession" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "FlowNodeStat" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "FlowNodeStat" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "FlowNodeStat" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "FlowNodeStat" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "flowId", + "entityType": "columns", + "schema": "public", + "table": "FlowNodeStat" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "analyticsId", + "entityType": "columns", + "schema": "public", + "table": "FlowNodeStat" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "nodeId", + "entityType": "columns", + "schema": "public", + "table": "FlowNodeStat" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "buttonId", + "entityType": "columns", + "schema": "public", + "table": "FlowNodeStat" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "FlowNodeStat" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactInboxId", + "entityType": "columns", + "schema": "public", + "table": "FlowNodeStat" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "eventType", + "entityType": "columns", + "schema": "public", + "table": "FlowNodeStat" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "errorContent", + "entityType": "columns", + "schema": "public", + "table": "FlowNodeStat" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "occurredAt", + "entityType": "columns", + "schema": "public", + "table": "FlowNodeStat" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "seenAt", + "entityType": "columns", + "schema": "public", + "table": "FlowNodeStat" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refId", + "entityType": "columns", + "schema": "public", + "table": "FlowNodeStat" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refType", + "entityType": "columns", + "schema": "public", + "table": "FlowNodeStat" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "FlowRun" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "FlowRun" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "FlowRun" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "FlowRun" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "flowId", + "entityType": "columns", + "schema": "public", + "table": "FlowRun" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "flowVersionId", + "entityType": "columns", + "schema": "public", + "table": "FlowRun" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "conversationId", + "entityType": "columns", + "schema": "public", + "table": "FlowRun" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "FlowVersion" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "FlowVersion" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "FlowVersion" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "FlowVersion" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "flowId", + "entityType": "columns", + "schema": "public", + "table": "FlowVersion" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": null, + "generated": null, + "identity": null, + "name": "nodes", + "entityType": "columns", + "schema": "public", + "table": "FlowVersion" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": null, + "generated": null, + "identity": null, + "name": "edges", + "entityType": "columns", + "schema": "public", + "table": "FlowVersion" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "isDraft", + "entityType": "columns", + "schema": "public", + "table": "FlowVersion" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isLatest", + "entityType": "columns", + "schema": "public", + "table": "FlowVersion" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "startNodeId", + "entityType": "columns", + "schema": "public", + "table": "FlowVersion" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Folder" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Folder" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Folder" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "Folder" + }, + { + "type": "folderType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "folderType", + "entityType": "columns", + "schema": "public", + "table": "Folder" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "parentId", + "entityType": "columns", + "schema": "public", + "table": "Folder" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "Folder" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isTrash", + "entityType": "columns", + "schema": "public", + "table": "Folder" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "", + "generated": null, + "identity": null, + "name": "paths", + "entityType": "columns", + "schema": "public", + "table": "Folder" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IgStoryAutomation" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IgStoryAutomation" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IgStoryAutomation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "IgStoryAutomation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IgStoryAutomation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "folderId", + "entityType": "columns", + "schema": "public", + "table": "IgStoryAutomation" + }, + { + "type": "igStoryAutomationType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "IgStoryAutomation" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "isActive", + "entityType": "columns", + "schema": "public", + "table": "IgStoryAutomation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "startTime", + "entityType": "columns", + "schema": "public", + "table": "IgStoryAutomation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "endTime", + "entityType": "columns", + "schema": "public", + "table": "IgStoryAutomation" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "repliesCount", + "entityType": "columns", + "schema": "public", + "table": "IgStoryAutomation" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{\"type\":\"all\",\"value\":[]}'", + "generated": null, + "identity": null, + "name": "story", + "entityType": "columns", + "schema": "public", + "table": "IgStoryAutomation" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{\"type\":\"none\",\"value\":null}'", + "generated": null, + "identity": null, + "name": "reply", + "entityType": "columns", + "schema": "public", + "table": "IgStoryAutomation" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{\"type\":\"all\",\"value\":[]}'", + "generated": null, + "identity": null, + "name": "includeKeywords", + "entityType": "columns", + "schema": "public", + "table": "IgStoryAutomation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Import" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Import" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Import" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "Import" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inboxId", + "entityType": "columns", + "schema": "public", + "table": "Import" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "Import" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fileId", + "entityType": "columns", + "schema": "public", + "table": "Import" + }, + { + "type": "importType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "Import" + }, + { + "type": "importFormat", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "format", + "entityType": "columns", + "schema": "public", + "table": "Import" + }, + { + "type": "importStatus", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "Import" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "meta", + "entityType": "columns", + "schema": "public", + "table": "Import" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "totalCount", + "entityType": "columns", + "schema": "public", + "table": "Import" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "processedCount", + "entityType": "columns", + "schema": "public", + "table": "Import" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "successCount", + "entityType": "columns", + "schema": "public", + "table": "Import" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "failedCount", + "entityType": "columns", + "schema": "public", + "table": "Import" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "errorMessage", + "entityType": "columns", + "schema": "public", + "table": "Import" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "errorSample", + "entityType": "columns", + "schema": "public", + "table": "Import" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "completedAt", + "entityType": "columns", + "schema": "public", + "table": "Import" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Inbox" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Inbox" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Inbox" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "Inbox" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "channel", + "entityType": "columns", + "schema": "public", + "table": "Inbox" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceId", + "entityType": "columns", + "schema": "public", + "table": "Inbox" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "Inbox" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'connected'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "Inbox" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "disconnectedAt", + "entityType": "columns", + "schema": "public", + "table": "Inbox" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "disconnectReason", + "entityType": "columns", + "schema": "public", + "table": "Inbox" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inboxId", + "entityType": "columns", + "schema": "public", + "table": "InboxContactStat" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "totalContacts", + "entityType": "columns", + "schema": "public", + "table": "InboxContactStat" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "InboxContactStat" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "InboxTeam" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "InboxTeam" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "InboxTeam" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "InboxTeam" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "InboxTeam" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "InboxTeamMember" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "InboxTeamMember" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "InboxTeamMember" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inboxTeamId", + "entityType": "columns", + "schema": "public", + "table": "InboxTeamMember" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "InboxTeamMember" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationActiveCampaign" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationActiveCampaign" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationActiveCampaign" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationActiveCampaign" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationActiveCampaign" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationActiveCampaign" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationApi" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationApi" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationApi" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationApi" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "IntegrationApi" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tokenHash", + "entityType": "columns", + "schema": "public", + "table": "IntegrationApi" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tokenPrefix", + "entityType": "columns", + "schema": "public", + "table": "IntegrationApi" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "callbackUrl", + "entityType": "columns", + "schema": "public", + "table": "IntegrationApi" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "enabled", + "entityType": "columns", + "schema": "public", + "table": "IntegrationApi" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationApi" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inboxId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationApi" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Integration" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Integration" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Integration" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "Integration" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationType", + "entityType": "columns", + "schema": "public", + "table": "Integration" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationClaude" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationClaude" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationClaude" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationClaude" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "autoReply", + "entityType": "columns", + "schema": "public", + "table": "IntegrationClaude" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationClaude" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationClaude" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "maxOutputTokens", + "entityType": "columns", + "schema": "public", + "table": "IntegrationClaude" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "model", + "entityType": "columns", + "schema": "public", + "table": "IntegrationClaude" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "prompt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationClaude" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "temperature", + "entityType": "columns", + "schema": "public", + "table": "IntegrationClaude" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationDeepseek" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationDeepseek" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationDeepseek" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationDeepseek" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "autoReply", + "entityType": "columns", + "schema": "public", + "table": "IntegrationDeepseek" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationDeepseek" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationDeepseek" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "maxOutputTokens", + "entityType": "columns", + "schema": "public", + "table": "IntegrationDeepseek" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "model", + "entityType": "columns", + "schema": "public", + "table": "IntegrationDeepseek" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "prompt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationDeepseek" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "temperature", + "entityType": "columns", + "schema": "public", + "table": "IntegrationDeepseek" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationDrip" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationDrip" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationDrip" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationDrip" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationDrip" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationDrip" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationFacebookAds" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationFacebookAds" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationFacebookAds" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationFacebookAds" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationFacebookAds" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationFacebookAds" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tokenExpiresAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationFacebookAds" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'active'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "IntegrationFacebookAds" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGemini" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGemini" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGemini" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGemini" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "autoReply", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGemini" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGemini" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGemini" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "maxOutputTokens", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGemini" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "model", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGemini" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "prompt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGemini" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "temperature", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGemini" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGetResponse" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGetResponse" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGetResponse" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGetResponse" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGetResponse" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGetResponse" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGoogleCalendar" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGoogleCalendar" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGoogleCalendar" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGoogleCalendar" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGoogleCalendar" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGoogleCalendar" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'primary'", + "generated": null, + "identity": null, + "name": "providerCalendarId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGoogleCalendar" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGoogleCalendar" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGoogleSheet" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGoogleSheet" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGoogleSheet" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGoogleSheet" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGoogleSheet" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationGoogleSheet" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userInfo", + "entityType": "columns", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "igId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pageId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "username", + "entityType": "columns", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "coexistEnabled", + "entityType": "columns", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "coexistAiReadsSyncedHistory", + "entityType": "columns", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "hasCapiScope", + "entityType": "columns", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "capiScopeCheckedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "datasetId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "capiAccessToken", + "entityType": "columns", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "capiDisconnectedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "capiTestEventCode", + "entityType": "columns", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "", + "generated": null, + "identity": null, + "name": "conversationStarters", + "entityType": "columns", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "", + "generated": null, + "identity": null, + "name": "persistentMenus", + "entityType": "columns", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inboxId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "welcomeFlowId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'instagram'", + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tokenRefreshError", + "entityType": "columns", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationKlaviyo" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationKlaviyo" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationKlaviyo" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationKlaviyo" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationKlaviyo" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationKlaviyo" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMailchimp" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMailchimp" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMailchimp" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMailchimp" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMailchimp" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMailchimp" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMailerLite" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMailerLite" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMailerLite" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMailerLite" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMailerLite" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMailerLite" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userInfo", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pageId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "", + "generated": null, + "identity": null, + "name": "conversationStarters", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "", + "generated": null, + "identity": null, + "name": "persistentMenus", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "", + "generated": null, + "identity": null, + "name": "personas", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "personaId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "coexistEnabled", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "coexistAiReadsSyncedHistory", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "hasCapiScope", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "capiScopeCheckedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "datasetId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "capiAccessToken", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "capiDisconnectedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "capiTestEventCode", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inboxId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "welcomeFlowId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "syncTagEnabledAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tokenRefreshError", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "catalogId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "catalogName", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "businessId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "encryptedAuth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "type": "metaCatalogAuthMode", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'oauth'", + "generated": null, + "identity": null, + "name": "authMode", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tokenExpiresAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "type": "metaCatalogConnectionStatus", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'active'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deletedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "type": "metaCatalogImportStatus", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'idle'", + "generated": null, + "identity": null, + "name": "importStatus", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "importTotalCount", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "importedCount", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "importFailedCount", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "importError", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastImportedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'VND'", + "generated": null, + "identity": null, + "name": "currency", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "storeUrl", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMoosend" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMoosend" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMoosend" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMoosend" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMoosend" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationMoosend" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenai" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenai" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenai" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenai" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "autoReply", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenai" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "autoReplyVoice", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenai" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "voice", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenai" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "prompt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenai" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "model", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenai" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "temperature", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenai" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "maxOutputTokens", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenai" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenai" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenai" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "aiAgentId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenai" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenaiCompatible" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenaiCompatible" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenaiCompatible" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenaiCompatible" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "autoReply", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenaiCompatible" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "baseURL", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenaiCompatible" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "defaultModel", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenaiCompatible" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "enabled", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenaiCompatible" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenaiCompatible" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenaiCompatible" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "preset", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenaiCompatible" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenaiCompatible" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenrouter" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenrouter" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenrouter" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenrouter" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "autoReply", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenrouter" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenrouter" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenrouter" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "maxOutputTokens", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenrouter" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "model", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenrouter" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "prompt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenrouter" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "temperature", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOpenrouter" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOutlookCalendar" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOutlookCalendar" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOutlookCalendar" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOutlookCalendar" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOutlookCalendar" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOutlookCalendar" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'primary'", + "generated": null, + "identity": null, + "name": "providerCalendarId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOutlookCalendar" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "IntegrationOutlookCalendar" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationSendGrid" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationSendGrid" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationSendGrid" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationSendGrid" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationSendGrid" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationSendGrid" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationSmtp" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationSmtp" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationSmtp" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationSmtp" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "IntegrationSmtp" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fromAddress", + "entityType": "columns", + "schema": "public", + "table": "IntegrationSmtp" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationSmtp" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inboxId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationSmtp" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationTelegram" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationTelegram" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationTelegram" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationTelegram" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "botId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationTelegram" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "IntegrationTelegram" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationTelegram" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inboxId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationTelegram" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationTiktok" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationTiktok" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationTiktok" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationTiktok" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "openId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationTiktok" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "IntegrationTiktok" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationTiktok" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inboxId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationTiktok" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tokenRefreshError", + "entityType": "columns", + "schema": "public", + "table": "IntegrationTiktok" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWebchat" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWebchat" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWebchat" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWebchat" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWebchat" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "enable", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWebchat" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "", + "generated": null, + "identity": null, + "name": "authorizedDomains", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWebchat" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "", + "generated": null, + "identity": null, + "name": "conversationStarters", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWebchat" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "", + "generated": null, + "identity": null, + "name": "persistentMenus", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWebchat" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "brandColor", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWebchat" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "hideHeader", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWebchat" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "showLogo", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWebchat" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "hideMessageInput", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWebchat" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "customCss", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWebchat" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWebchat" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inboxId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWebchat" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "welcomeFlowId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWebchat" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "phoneNumberId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "wabaId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "businessId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "displayPhoneNumber", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "coexistEnabled", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "coexistAiReadsSyncedHistory", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isCoexist", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "platformType", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "historyDeclined", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "hasCapiScope", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "capiScopeCheckedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "datasetId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "capiAccessToken", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "capiDisconnectedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "capiTestEventCode", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "whatsappRegistrationStatus", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'pending_verification'", + "generated": null, + "identity": null, + "name": "registrationStatus", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "registrationError", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "verificationCodeRequestedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tokenRefreshError", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inboxId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "IntegrationZalo" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationZalo" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationZalo" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "IntegrationZalo" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "oaId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationZalo" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "IntegrationZalo" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationZalo" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inboxId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationZalo" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fallbackFlowId", + "entityType": "columns", + "schema": "public", + "table": "IntegrationZalo" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "syncTagEnabledAt", + "entityType": "columns", + "schema": "public", + "table": "IntegrationZalo" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tokenRefreshError", + "entityType": "columns", + "schema": "public", + "table": "IntegrationZalo" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "MagicLink" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "MagicLink" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "MagicLink" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "MagicLink" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "MagicLink" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "url", + "entityType": "columns", + "schema": "public", + "table": "MagicLink" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "MagicLinkStat" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "linkId", + "entityType": "columns", + "schema": "public", + "table": "MagicLinkStat" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "MagicLinkStat" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactInboxId", + "entityType": "columns", + "schema": "public", + "table": "MagicLinkStat" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "occurredAt", + "entityType": "columns", + "schema": "public", + "table": "MagicLinkStat" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "MagicLinkStat" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "MediaLibraryFile" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "MediaLibraryFile" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "MediaLibraryFile" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "MediaLibraryFile" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "path", + "entityType": "columns", + "schema": "public", + "table": "MediaLibraryFile" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "mimeType", + "entityType": "columns", + "schema": "public", + "table": "MediaLibraryFile" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "size", + "entityType": "columns", + "schema": "public", + "table": "MediaLibraryFile" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isFavourite", + "entityType": "columns", + "schema": "public", + "table": "MediaLibraryFile" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastAccessedAt", + "entityType": "columns", + "schema": "public", + "table": "MediaLibraryFile" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "MediaLibraryFile" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "folderId", + "entityType": "columns", + "schema": "public", + "table": "MediaLibraryFile" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "MediaLibraryFolder" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "MediaLibraryFolder" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "MediaLibraryFolder" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "MediaLibraryFolder" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "MediaLibraryFolder" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Message" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Message" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Message" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "conversationId", + "entityType": "columns", + "schema": "public", + "table": "Message" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactInboxId", + "entityType": "columns", + "schema": "public", + "table": "Message" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "Message" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "text", + "entityType": "columns", + "schema": "public", + "table": "Message" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contentAttributes", + "entityType": "columns", + "schema": "public", + "table": "Message" + }, + { + "type": "messageType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "messageType", + "entityType": "columns", + "schema": "public", + "table": "Message" + }, + { + "type": "contentType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contentType", + "entityType": "columns", + "schema": "public", + "table": "Message" + }, + { + "type": "senderType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "senderType", + "entityType": "columns", + "schema": "public", + "table": "Message" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "senderId", + "entityType": "columns", + "schema": "public", + "table": "Message" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceId", + "entityType": "columns", + "schema": "public", + "table": "Message" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deletedAt", + "entityType": "columns", + "schema": "public", + "table": "Message" + }, + { + "type": "messageKind", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'message'", + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "Message" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "parentId", + "entityType": "columns", + "schema": "public", + "table": "Message" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "attributes", + "entityType": "columns", + "schema": "public", + "table": "Message" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sendError", + "entityType": "columns", + "schema": "public", + "table": "Message" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "MessageCleanup" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "MessageCleanup" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "MessageCleanup" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "MessageCleanup" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "MessageCleanup" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactInboxId", + "entityType": "columns", + "schema": "public", + "table": "MessageCleanup" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inboxId", + "entityType": "columns", + "schema": "public", + "table": "MessageCleanup" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceId", + "entityType": "columns", + "schema": "public", + "table": "MessageCleanup" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "conversationIds", + "entityType": "columns", + "schema": "public", + "table": "MessageCleanup" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sinceTime", + "entityType": "columns", + "schema": "public", + "table": "MessageCleanup" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "deletedAt", + "entityType": "columns", + "schema": "public", + "table": "MessageCleanup" + }, + { + "type": "MessageCleanupStatus", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "MessageCleanup" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "attempts", + "entityType": "columns", + "schema": "public", + "table": "MessageCleanup" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastError", + "entityType": "columns", + "schema": "public", + "table": "MessageCleanup" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "processedAt", + "entityType": "columns", + "schema": "public", + "table": "MessageCleanup" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "type": "messagingAdChannel", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "channel", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationWhatsappId", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationMessengerId", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationInstagramId", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "adAccountId", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "type": "messagingAdCreateState", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "createState", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "type": "messagingAdPublishState", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "publishState", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "metaCampaignId", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "metaAdSetId", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "metaAdCreativeId", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "metaAdId", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "input", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastError", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "cleanupError", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "createdBy", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdsConnection" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdsConnection" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdsConnection" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdsConnection" + }, + { + "type": "messagingAdChannel", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "channel", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdsConnection" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationWhatsappId", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdsConnection" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationMessengerId", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdsConnection" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationInstagramId", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdsConnection" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdsConnection" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'active'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "MessagingAdsConnection" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "MessengerMessageTemplate" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "MessengerMessageTemplate" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "MessengerMessageTemplate" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "MessengerMessageTemplate" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationMessengerId", + "entityType": "columns", + "schema": "public", + "table": "MessengerMessageTemplate" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceId", + "entityType": "columns", + "schema": "public", + "table": "MessengerMessageTemplate" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "language", + "entityType": "columns", + "schema": "public", + "table": "MessengerMessageTemplate" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "category", + "entityType": "columns", + "schema": "public", + "table": "MessengerMessageTemplate" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "MessengerMessageTemplate" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'POSITIONAL'", + "generated": null, + "identity": null, + "name": "parameterFormat", + "entityType": "columns", + "schema": "public", + "table": "MessengerMessageTemplate" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "components", + "entityType": "columns", + "schema": "public", + "table": "MessengerMessageTemplate" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "channel", + "entityType": "columns", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationId", + "entityType": "columns", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactInboxId", + "entityType": "columns", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "eventName", + "entityType": "columns", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "currency", + "entityType": "columns", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contentCategory", + "entityType": "columns", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contentName", + "entityType": "columns", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'business_messaging'", + "generated": null, + "identity": null, + "name": "actionSource", + "entityType": "columns", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contentType", + "entityType": "columns", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contentIds", + "entityType": "columns", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "source", + "entityType": "columns", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceKey", + "entityType": "columns", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "occurredAt", + "entityType": "columns", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "capiStatus", + "entityType": "columns", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "capiSentAt", + "entityType": "columns", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "capiError", + "entityType": "columns", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogItem" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogItem" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogItem" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationMetaCatalogId", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogItem" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "productId", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogItem" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "catalogId", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogItem" + }, + { + "type": "metaCatalogItemDirection", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'push'", + "generated": null, + "identity": null, + "name": "direction", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogItem" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "retailerId", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogItem" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastSyncedFingerprint", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogItem" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastSyncedAt", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogItem" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationMetaCatalogId", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "type": "metaCatalogSyncStatus", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'queued'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "type": "metaCatalogItemDirection", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'push'", + "generated": null, + "identity": null, + "name": "direction", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "catalogId", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "type": "metaCatalogSyncScope", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'all'", + "generated": null, + "identity": null, + "name": "scope", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "categoryId", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "selectedProductIds", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "handles", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "submissionLeaseId", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "totalCount", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "succeededCount", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "failedCount", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "skippedCount", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "itemErrors", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "skippedItems", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "pollAttempt", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "error", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "startedAt", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "finishedAt", + "entityType": "columns", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Minigame" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Minigame" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Minigame" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "Minigame" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "Minigame" + }, + { + "type": "minigameType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "Minigame" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "enabled", + "entityType": "columns", + "schema": "public", + "table": "Minigame" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "generalSettings", + "entityType": "columns", + "schema": "public", + "table": "Minigame" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "appearance", + "entityType": "columns", + "schema": "public", + "table": "Minigame" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "playerSettings", + "entityType": "columns", + "schema": "public", + "table": "Minigame" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "prizeSettings", + "entityType": "columns", + "schema": "public", + "table": "Minigame" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "winningMessageSettings", + "entityType": "columns", + "schema": "public", + "table": "Minigame" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "nonWinningMessageSettings", + "entityType": "columns", + "schema": "public", + "table": "Minigame" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "playsCount", + "entityType": "columns", + "schema": "public", + "table": "Minigame" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "participantsCount", + "entityType": "columns", + "schema": "public", + "table": "Minigame" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "winnersCount", + "entityType": "columns", + "schema": "public", + "table": "Minigame" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "sharesCount", + "entityType": "columns", + "schema": "public", + "table": "Minigame" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "MinigameContact" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "MinigameContact" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "MinigameContact" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "minigameId", + "entityType": "columns", + "schema": "public", + "table": "MinigameContact" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "MinigameContact" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "openedAt", + "entityType": "columns", + "schema": "public", + "table": "MinigameContact" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "played", + "entityType": "columns", + "schema": "public", + "table": "MinigameContact" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "remaining", + "entityType": "columns", + "schema": "public", + "table": "MinigameContact" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "sharesCount", + "entityType": "columns", + "schema": "public", + "table": "MinigameContact" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "referrerContactId", + "entityType": "columns", + "schema": "public", + "table": "MinigameContact" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactInboxId", + "entityType": "columns", + "schema": "public", + "table": "MinigameContact" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "MinigamePlay" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "MinigamePlay" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "MinigamePlay" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "minigameId", + "entityType": "columns", + "schema": "public", + "table": "MinigamePlay" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "MinigamePlay" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "isWinning", + "entityType": "columns", + "schema": "public", + "table": "MinigamePlay" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "prizeId", + "entityType": "columns", + "schema": "public", + "table": "MinigamePlay" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "prizeName", + "entityType": "columns", + "schema": "public", + "table": "MinigamePlay" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "PlatformCredential" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "PlatformCredential" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "PlatformCredential" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "PlatformCredential" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "PlatformCredential" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publicConfig", + "entityType": "columns", + "schema": "public", + "table": "PlatformCredential" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "public", + "table": "PlatformCredential" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "livemode", + "entityType": "columns", + "schema": "public", + "table": "PlatformCredential" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "usePlatformCredential", + "entityType": "columns", + "schema": "public", + "table": "PlatformCredential" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isVerified", + "entityType": "columns", + "schema": "public", + "table": "PlatformCredential" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "verifiedAt", + "entityType": "columns", + "schema": "public", + "table": "PlatformCredential" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastUsedAt", + "entityType": "columns", + "schema": "public", + "table": "PlatformCredential" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "shortDescription", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "longDescription", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "price", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "taxes", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "discount", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'USD'", + "generated": null, + "identity": null, + "name": "currency", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "productUrl", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sku", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "inventoryPolicy", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'dont_track'", + "generated": null, + "identity": null, + "name": "inventoryPolicy", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "inventoryQuantity", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "allowOutOfStockPurchase", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "images", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "tags", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "vendor", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "10", + "generated": null, + "identity": null, + "name": "rank", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "categoryId", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "subcategoryId", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "isActive", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "isSearchable", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "allowSpecialRequest", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isAddonOnly", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "Product" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "ProductAddon" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "ProductAddon" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "ProductAddon" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "productId", + "entityType": "columns", + "schema": "public", + "table": "ProductAddon" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "ProductAddon" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "maxSelections", + "entityType": "columns", + "schema": "public", + "table": "ProductAddon" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "addonProductIds", + "entityType": "columns", + "schema": "public", + "table": "ProductAddon" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "ProductCategory" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "ProductCategory" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "ProductCategory" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "ProductCategory" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "parentId", + "entityType": "columns", + "schema": "public", + "table": "ProductCategory" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "ProductCategory" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "10", + "generated": null, + "identity": null, + "name": "rank", + "entityType": "columns", + "schema": "public", + "table": "ProductCategory" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "ProductVariant" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "ProductVariant" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "ProductVariant" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "productId", + "entityType": "columns", + "schema": "public", + "table": "ProductVariant" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "combination", + "entityType": "columns", + "schema": "public", + "table": "ProductVariant" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "price", + "entityType": "columns", + "schema": "public", + "table": "ProductVariant" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "isEnabled", + "entityType": "columns", + "schema": "public", + "table": "ProductVariant" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "ProductVariantOption" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "ProductVariantOption" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "ProductVariantOption" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "productId", + "entityType": "columns", + "schema": "public", + "table": "ProductVariantOption" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "ProductVariantOption" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "values", + "entityType": "columns", + "schema": "public", + "table": "ProductVariantOption" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "10", + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "ProductVariantOption" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireAnswer" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireAnswer" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireAnswer" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "submissionId", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireAnswer" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "questionId", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireAnswer" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "questionIdSnapshot", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireAnswer" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "questionTitleSnapshot", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireAnswer" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "questionTypeSnapshot", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireAnswer" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "labelSnapshot", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireAnswer" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireAnswer" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pointsEarned", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireAnswer" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "attemptCount", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireAnswer" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "answeredAt", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireAnswer" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Questionnaire" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Questionnaire" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Questionnaire" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "Questionnaire" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "enableScore", + "entityType": "columns", + "schema": "public", + "table": "Questionnaire" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "enableRetryMessages", + "entityType": "columns", + "schema": "public", + "table": "Questionnaire" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "enableCustomFieldMapping", + "entityType": "columns", + "schema": "public", + "table": "Questionnaire" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deletedAt", + "entityType": "columns", + "schema": "public", + "table": "Questionnaire" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "triggerFlowId", + "entityType": "columns", + "schema": "public", + "table": "Questionnaire" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "Questionnaire" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireQuestion" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireQuestion" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireQuestion" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "questionnaireId", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireQuestion" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireQuestion" + }, + { + "type": "questionnaireQuestionType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireQuestion" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "active", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireQuestion" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "image", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireQuestion" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "orderNo", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireQuestion" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "point", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireQuestion" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "retryMessage", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireQuestion" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "customFieldId", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireQuestion" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "systemFieldKey", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireQuestion" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "config", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireQuestion" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deletedAt", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireQuestion" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireSubmission" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireSubmission" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireSubmission" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireSubmission" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "questionnaireId", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireSubmission" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireSubmission" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "conversationId", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireSubmission" + }, + { + "type": "questionnaireSubmissionStatus", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'inProgress'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireSubmission" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "totalPoints", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireSubmission" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "currentQuestionId", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireSubmission" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "currentQuestionSentAt", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireSubmission" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastAnsweredMessageId", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireSubmission" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "startedAt", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireSubmission" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "completedAt", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireSubmission" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "cancelledAt", + "entityType": "columns", + "schema": "public", + "table": "QuestionnaireSubmission" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "RefLinkStat" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "linkId", + "entityType": "columns", + "schema": "public", + "table": "RefLinkStat" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "RefLinkStat" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactInboxId", + "entityType": "columns", + "schema": "public", + "table": "RefLinkStat" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "occurredAt", + "entityType": "columns", + "schema": "public", + "table": "RefLinkStat" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "RefLinkStat" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Reflink" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Reflink" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Reflink" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "Reflink" + }, + { + "type": "ReflinkType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'refLink'", + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "Reflink" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "flowId", + "entityType": "columns", + "schema": "public", + "table": "Reflink" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "Reflink" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "customFieldId", + "entityType": "columns", + "schema": "public", + "table": "Reflink" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "qrStyles", + "entityType": "columns", + "schema": "public", + "table": "Reflink" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "SavedReply" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "SavedReply" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "SavedReply" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "shortcut", + "entityType": "columns", + "schema": "public", + "table": "SavedReply" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "text", + "entityType": "columns", + "schema": "public", + "table": "SavedReply" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "SavedReply" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Sequence" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Sequence" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Sequence" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "Sequence" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "folderId", + "entityType": "columns", + "schema": "public", + "table": "Sequence" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "active", + "entityType": "columns", + "schema": "public", + "table": "Sequence" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "subscribers", + "entityType": "columns", + "schema": "public", + "table": "Sequence" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "messages", + "entityType": "columns", + "schema": "public", + "table": "Sequence" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "Sequence" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "runAtMs", + "entityType": "columns", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "bucket", + "entityType": "columns", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "idempotencyKey", + "entityType": "columns", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "attempt", + "entityType": "columns", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastError", + "entityType": "columns", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lockedAt", + "entityType": "columns", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lockOwner", + "entityType": "columns", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "completedAt", + "entityType": "columns", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deliveredAt", + "entityType": "columns", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "seenAt", + "entityType": "columns", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "clickedAt", + "entityType": "columns", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "failedAt", + "entityType": "columns", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "errorContent", + "entityType": "columns", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sequenceId", + "entityType": "columns", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactInboxId", + "entityType": "columns", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "stepId", + "entityType": "columns", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "enrollmentId", + "entityType": "columns", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": { + "as": "case when \"seenAt\" is null then false when \"deliveredAt\" is null then false else \"seenAt\" >= \"deliveredAt\" end", + "type": "stored" + }, + "identity": null, + "name": "isRead", + "entityType": "columns", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "SequenceStep" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "SequenceStep" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "SequenceStep" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "order", + "entityType": "columns", + "schema": "public", + "table": "SequenceStep" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "delayDays", + "entityType": "columns", + "schema": "public", + "table": "SequenceStep" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "delayMinutes", + "entityType": "columns", + "schema": "public", + "table": "SequenceStep" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "delayUnit", + "entityType": "columns", + "schema": "public", + "table": "SequenceStep" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "specificDateTime", + "entityType": "columns", + "schema": "public", + "table": "SequenceStep" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "isActive", + "entityType": "columns", + "schema": "public", + "table": "SequenceStep" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "anytime", + "entityType": "columns", + "schema": "public", + "table": "SequenceStep" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sendTimeStart", + "entityType": "columns", + "schema": "public", + "table": "SequenceStep" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sendTimeEnd", + "entityType": "columns", + "schema": "public", + "table": "SequenceStep" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "'[\"monday\",\"tuesday\",\"wednesday\",\"thursday\",\"friday\",\"saturday\",\"sunday\"]'", + "generated": null, + "identity": null, + "name": "sendDays", + "entityType": "columns", + "schema": "public", + "table": "SequenceStep" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "flowId", + "entityType": "columns", + "schema": "public", + "table": "SequenceStep" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sequenceId", + "entityType": "columns", + "schema": "public", + "table": "SequenceStep" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Spreadsheet" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Spreadsheet" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Spreadsheet" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "Spreadsheet" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "Spreadsheet" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "url", + "entityType": "columns", + "schema": "public", + "table": "Spreadsheet" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "spreadsheetId", + "entityType": "columns", + "schema": "public", + "table": "Spreadsheet" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "SystemField" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "SystemField" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "SystemField" + }, + { + "type": "SystemFieldType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "SystemField" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "payload", + "entityType": "columns", + "schema": "public", + "table": "SystemField" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Tag" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Tag" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Tag" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "Tag" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deletedAt", + "entityType": "columns", + "schema": "public", + "table": "Tag" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "folderId", + "entityType": "columns", + "schema": "public", + "table": "Tag" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "Tag" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "TagChannel" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "TagChannel" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "TagChannel" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "TagChannel" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tagId", + "entityType": "columns", + "schema": "public", + "table": "TagChannel" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "channelType", + "entityType": "columns", + "schema": "public", + "table": "TagChannel" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationId", + "entityType": "columns", + "schema": "public", + "table": "TagChannel" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "externalLabelId", + "entityType": "columns", + "schema": "public", + "table": "TagChannel" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Template" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Template" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Template" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "Template" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tenantId", + "entityType": "columns", + "schema": "public", + "table": "Template" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "Template" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "Template" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "imageUrl", + "entityType": "columns", + "schema": "public", + "table": "Template" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publisherName", + "entityType": "columns", + "schema": "public", + "table": "Template" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "youtubeVideoId", + "entityType": "columns", + "schema": "public", + "table": "Template" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "testLink", + "entityType": "columns", + "schema": "public", + "table": "Template" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "selection", + "entityType": "columns", + "schema": "public", + "table": "Template" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "payload", + "entityType": "columns", + "schema": "public", + "table": "Template" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "categoryCounts", + "entityType": "columns", + "schema": "public", + "table": "Template" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "formatVersion", + "entityType": "columns", + "schema": "public", + "table": "Template" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "shareToken", + "entityType": "columns", + "schema": "public", + "table": "Template" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "shareEnabled", + "entityType": "columns", + "schema": "public", + "table": "Template" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "shareExpiresAt", + "entityType": "columns", + "schema": "public", + "table": "Template" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "defaultPermissions", + "entityType": "columns", + "schema": "public", + "table": "Template" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "createInstallFolder", + "entityType": "columns", + "schema": "public", + "table": "Template" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "defaultAutoUpdate", + "entityType": "columns", + "schema": "public", + "table": "Template" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "installCount", + "entityType": "columns", + "schema": "public", + "table": "Template" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "createdBy", + "entityType": "columns", + "schema": "public", + "table": "Template" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deletedAt", + "entityType": "columns", + "schema": "public", + "table": "Template" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "templateId", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "templateName", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceWorkspaceId", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "formatVersion", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "type": "templateInstallationStatus", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "permissions", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "warnings", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "warningCount", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "errorMessage", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "resourceCount", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "installFolderId", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "autoUpdate", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceUpdatedAt", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "installedBy", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "completedAt", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstalledResource" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstalledResource" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstalledResource" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "installationId", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstalledResource" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstalledResource" + }, + { + "type": "templateResourceCategory", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "category", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstalledResource" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "resourceKind", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstalledResource" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "resourceId", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstalledResource" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceResourceId", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstalledResource" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "wasExisting", + "entityType": "columns", + "schema": "public", + "table": "TemplateInstalledResource" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Trigger" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Trigger" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Trigger" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "Trigger" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "active", + "entityType": "columns", + "schema": "public", + "table": "Trigger" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "folderId", + "entityType": "columns", + "schema": "public", + "table": "Trigger" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "", + "generated": null, + "identity": null, + "name": "actions", + "entityType": "columns", + "schema": "public", + "table": "Trigger" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "Trigger" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Condition" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Condition" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Condition" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "triggerId", + "entityType": "columns", + "schema": "public", + "table": "Condition" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "webhookId", + "entityType": "columns", + "schema": "public", + "table": "Condition" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "Condition" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceId", + "entityType": "columns", + "schema": "public", + "table": "Condition" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "operator", + "entityType": "columns", + "schema": "public", + "table": "Condition" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "public", + "table": "Condition" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "TriggerContactHistory" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "TriggerContactHistory" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "TriggerContactHistory" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "triggerId", + "entityType": "columns", + "schema": "public", + "table": "TriggerContactHistory" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "TriggerContactHistory" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "TriggerContactHistory" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "firstEnteredAt", + "entityType": "columns", + "schema": "public", + "table": "TriggerContactHistory" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "TriggerExecution" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "TriggerExecution" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "TriggerExecution" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "executedAt", + "entityType": "columns", + "schema": "public", + "table": "TriggerExecution" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "triggerId", + "entityType": "columns", + "schema": "public", + "table": "TriggerExecution" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "TriggerExecution" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "TriggerExecution" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "TriggerStat" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "TriggerStat" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "TriggerStat" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "triggerId", + "entityType": "columns", + "schema": "public", + "table": "TriggerStat" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "TriggerStat" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "date", + "entityType": "columns", + "schema": "public", + "table": "TriggerStat" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "totalContacts", + "entityType": "columns", + "schema": "public", + "table": "TriggerStat" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "successCount", + "entityType": "columns", + "schema": "public", + "table": "TriggerStat" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "failureCount", + "entityType": "columns", + "schema": "public", + "table": "TriggerStat" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "totalExecutions", + "entityType": "columns", + "schema": "public", + "table": "TriggerStat" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "UserDeviceToken" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "UserDeviceToken" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "UserDeviceToken" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "UserDeviceToken" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "UserDeviceToken" + }, + { + "type": "devicePlatform", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "platform", + "entityType": "columns", + "schema": "public", + "table": "UserDeviceToken" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "public", + "table": "UserDeviceToken" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "lastSeenAt", + "entityType": "columns", + "schema": "public", + "table": "UserDeviceToken" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "UserPersistentMenu" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "UserPersistentMenu" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "UserPersistentMenu" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "UserPersistentMenu" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "", + "generated": null, + "identity": null, + "name": "menus", + "entityType": "columns", + "schema": "public", + "table": "UserPersistentMenu" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "UserPersistentMenu" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Webhook" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Webhook" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Webhook" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "Webhook" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "active", + "entityType": "columns", + "schema": "public", + "table": "Webhook" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "folderId", + "entityType": "columns", + "schema": "public", + "table": "Webhook" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "url", + "entityType": "columns", + "schema": "public", + "table": "Webhook" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "Webhook" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "WebhookExecution" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "WebhookExecution" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "WebhookExecution" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "executedAt", + "entityType": "columns", + "schema": "public", + "table": "WebhookExecution" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "webhookId", + "entityType": "columns", + "schema": "public", + "table": "WebhookExecution" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contactId", + "entityType": "columns", + "schema": "public", + "table": "WebhookExecution" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "WebhookExecution" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "WhatsappBusinessAccount" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "WhatsappBusinessAccount" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "WhatsappBusinessAccount" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "WhatsappBusinessAccount" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "wabaId", + "entityType": "columns", + "schema": "public", + "table": "WhatsappBusinessAccount" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "businessId", + "entityType": "columns", + "schema": "public", + "table": "WhatsappBusinessAccount" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "credential", + "entityType": "columns", + "schema": "public", + "table": "WhatsappBusinessAccount" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "grantedScopes", + "entityType": "columns", + "schema": "public", + "table": "WhatsappBusinessAccount" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "scopeCheckedAt", + "entityType": "columns", + "schema": "public", + "table": "WhatsappBusinessAccount" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provisionedAt", + "entityType": "columns", + "schema": "public", + "table": "WhatsappBusinessAccount" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "creditLineId", + "entityType": "columns", + "schema": "public", + "table": "WhatsappBusinessAccount" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "revision", + "entityType": "columns", + "schema": "public", + "table": "WhatsappBusinessAccount" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "WhatsappCoexistStaging" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "WhatsappCoexistStaging" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "WhatsappCoexistStaging" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "phoneNumberId", + "entityType": "columns", + "schema": "public", + "table": "WhatsappCoexistStaging" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "payload", + "entityType": "columns", + "schema": "public", + "table": "WhatsappCoexistStaging" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "payloadHash", + "entityType": "columns", + "schema": "public", + "table": "WhatsappCoexistStaging" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "processedAt", + "entityType": "columns", + "schema": "public", + "table": "WhatsappCoexistStaging" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "parseFailedAt", + "entityType": "columns", + "schema": "public", + "table": "WhatsappCoexistStaging" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "WhatsappFlow" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "WhatsappFlow" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "WhatsappFlow" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "WhatsappFlow" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationWhatsappId", + "entityType": "columns", + "schema": "public", + "table": "WhatsappFlow" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceId", + "entityType": "columns", + "schema": "public", + "table": "WhatsappFlow" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "WhatsappFlow" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "categories", + "entityType": "columns", + "schema": "public", + "table": "WhatsappFlow" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "validationErrors", + "entityType": "columns", + "schema": "public", + "table": "WhatsappFlow" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "completedCount", + "entityType": "columns", + "schema": "public", + "table": "WhatsappFlow" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "screens", + "entityType": "columns", + "schema": "public", + "table": "WhatsappFlow" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "WhatsappMessageTemplate" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "WhatsappMessageTemplate" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "WhatsappMessageTemplate" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "WhatsappMessageTemplate" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "integrationWhatsappId", + "entityType": "columns", + "schema": "public", + "table": "WhatsappMessageTemplate" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceId", + "entityType": "columns", + "schema": "public", + "table": "WhatsappMessageTemplate" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "language", + "entityType": "columns", + "schema": "public", + "table": "WhatsappMessageTemplate" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "category", + "entityType": "columns", + "schema": "public", + "table": "WhatsappMessageTemplate" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "WhatsappMessageTemplate" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "components", + "entityType": "columns", + "schema": "public", + "table": "WhatsappMessageTemplate" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "WhatsappSignupSession" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "WhatsappSignupSession" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "WhatsappSignupSession" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "WhatsappSignupSession" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ownerId", + "entityType": "columns", + "schema": "public", + "table": "WhatsappSignupSession" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "WhatsappSignupSession" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "wabaId", + "entityType": "columns", + "schema": "public", + "table": "WhatsappSignupSession" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "businessId", + "entityType": "columns", + "schema": "public", + "table": "WhatsappSignupSession" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "encryptedAccessToken", + "entityType": "columns", + "schema": "public", + "table": "WhatsappSignupSession" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "apiVersion", + "entityType": "columns", + "schema": "public", + "table": "WhatsappSignupSession" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": null, + "generated": null, + "identity": null, + "name": "candidatePhoneNumberIds", + "entityType": "columns", + "schema": "public", + "table": "WhatsappSignupSession" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "claimedPhoneNumberIds", + "entityType": "columns", + "schema": "public", + "table": "WhatsappSignupSession" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "WhatsappSignupSession" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "consumedAt", + "entityType": "columns", + "schema": "public", + "table": "WhatsappSignupSession" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "Workspace" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "Workspace" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "Workspace" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "Workspace" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "defaultReply", + "entityType": "columns", + "schema": "public", + "table": "Workspace" + }, + { + "type": "defaultReplyFrequency", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'allTime'", + "generated": null, + "identity": null, + "name": "defaultReplyFrequency", + "entityType": "columns", + "schema": "public", + "table": "Workspace" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "targetCountry", + "entityType": "columns", + "schema": "public", + "table": "Workspace" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'en'", + "generated": null, + "identity": null, + "name": "language", + "entityType": "columns", + "schema": "public", + "table": "Workspace" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'UTC'", + "generated": null, + "identity": null, + "name": "timezone", + "entityType": "columns", + "schema": "public", + "table": "Workspace" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'#016DFF'", + "generated": null, + "identity": null, + "name": "brandColor", + "entityType": "columns", + "schema": "public", + "table": "Workspace" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "developmentMode", + "entityType": "columns", + "schema": "public", + "table": "Workspace" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "smartResponseDelaySeconds", + "entityType": "columns", + "schema": "public", + "table": "Workspace" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "isActive", + "entityType": "columns", + "schema": "public", + "table": "Workspace" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "startTime", + "entityType": "columns", + "schema": "public", + "table": "Workspace" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "endTime", + "entityType": "columns", + "schema": "public", + "table": "Workspace" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "logo", + "entityType": "columns", + "schema": "public", + "table": "Workspace" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "scheduledDeletionAt", + "entityType": "columns", + "schema": "public", + "table": "Workspace" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "supportAccessUntil", + "entityType": "columns", + "schema": "public", + "table": "Workspace" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "capiLimitedDataUse", + "entityType": "columns", + "schema": "public", + "table": "Workspace" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "public", + "table": "Workspace" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ownerId", + "entityType": "columns", + "schema": "public", + "table": "Workspace" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "tenantId", + "entityType": "columns", + "schema": "public", + "table": "Workspace" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceApiToken" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceApiToken" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceApiToken" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceApiToken" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceApiToken" + }, + { + "type": "WorkspaceApiTokenPermission", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "permission", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceApiToken" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tokenHash", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceApiToken" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tokenPrefix", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceApiToken" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isDefault", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceApiToken" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "encryptedToken", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceApiToken" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 1, + "default": null, + "generated": null, + "identity": null, + "name": "scopes", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceApiToken" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceMac" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceMac" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceMac" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceMac" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "periodStart", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceMac" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "periodEnd", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceMac" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "macCount", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceMac" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceMember" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceMember" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceMember" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspaceId", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceMember" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceMember" + }, + { + "type": "workspaceMemberRole", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceMember" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "notificationChannels", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceMember" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "notificationTypes", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceMember" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "permissions", + "entityType": "columns", + "schema": "public", + "table": "WorkspaceMember" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "isActive", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MessageShard_isActive_idx", + "entityType": "indexes", + "schema": "public", + "table": "MessageShard" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "shardKey", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MessageShard_shardKey_idx", + "entityType": "indexes", + "schema": "public", + "table": "MessageShard" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "startTime", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "endTime", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ShardTimeRange_time_lookup_idx", + "entityType": "indexes", + "schema": "public", + "table": "ShardTimeRange" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "shardId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ShardTimeRange_shardId_idx", + "entityType": "indexes", + "schema": "public", + "table": "ShardTimeRange" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "integrationWhatsappId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "source", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "sourceEventId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AdsConversionEvent_workspace_integration_source_sourceEventId_key", + "entityType": "indexes", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "integrationWhatsappId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "source", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "sourceEventId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"channel\" = 'whatsapp'", + "with": "", + "method": "btree", + "concurrently": false, + "name": "AdsConversionEvent_ws_whatsapp_source_sourceEventId_key", + "entityType": "indexes", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "integrationMessengerId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "source", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "sourceEventId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"channel\" = 'messenger'", + "with": "", + "method": "btree", + "concurrently": false, + "name": "AdsConversionEvent_ws_messenger_source_sourceEventId_key", + "entityType": "indexes", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "integrationInstagramId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "source", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "sourceEventId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"channel\" = 'instagram'", + "with": "", + "method": "btree", + "concurrently": false, + "name": "AdsConversionEvent_ws_instagram_source_sourceEventId_key", + "entityType": "indexes", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "eventType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "occurredAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AdsConversionEvent_workspaceId_eventType_occurredAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactInboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AdsConversionEvent_contactInboxId_idx", + "entityType": "indexes", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "occurredAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AdsConversionEvent_workspaceId_occurredAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "adId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "occurredAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AdsConversionEvent_workspaceId_adId_occurredAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "channel", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "enabled", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AdsConversionRule_workspaceId_channel_enabled_idx", + "entityType": "indexes", + "schema": "public", + "table": "AdsConversionRule" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "sourceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AIConversationEmbedding_sourceId_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "AIConversationEmbedding" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "sourceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "chunkIndex", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AIConversationEmbedding_sourceId_chunkIndex_key", + "entityType": "indexes", + "schema": "public", + "table": "AIConversationEmbedding" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "embedding", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "vector_cosine_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "hnsw", + "concurrently": false, + "name": "AIConversationEmbedding_embedding_idx", + "entityType": "indexes", + "schema": "public", + "table": "AIConversationEmbedding" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "conversationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AIConversationEmbedding_conversationId_idx", + "entityType": "indexes", + "schema": "public", + "table": "AIConversationEmbedding" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "conversationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "sourceType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AIConversationSource_lookup_idx", + "entityType": "indexes", + "schema": "public", + "table": "AIConversationSource" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "sourceType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "sourceKey", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AIConversationSource_workspaceId_sourceType_sourceKey_key", + "entityType": "indexes", + "schema": "public", + "table": "AIConversationSource" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "messageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AIConversationSource_messageId_idx", + "entityType": "indexes", + "schema": "public", + "table": "AIConversationSource" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "conversationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AIConversationSource_conversationId_idx", + "entityType": "indexes", + "schema": "public", + "table": "AIConversationSource" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AIEmbedding_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "AIEmbedding" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "occurredAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AnalyticsBotMessageEvent_workspaceId_occurredAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "AnalyticsBotMessageEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "aiProvider", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "occurredAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AnalyticsBotMessageEvent_workspaceId_aiProvider_occurredAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "AnalyticsBotMessageEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "hasResponse", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "result", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "occurredAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AnalyticsBotMessageEvent_workspaceId_hasResponse_result_occurredAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "AnalyticsBotMessageEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "broadcastId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "eventType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "occurredAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AnalyticsBroadcastEvent_workspaceId_broadcastId_eventType_occurredAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "AnalyticsBroadcastEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "occurredAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "eventType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AnalyticsContactEvent_workspaceId_occurredAt_eventType_idx", + "entityType": "indexes", + "schema": "public", + "table": "AnalyticsContactEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "eventType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "occurredAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AnalyticsContactEvent_workspaceId_eventType_occurredAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "AnalyticsContactEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "adminId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "occurredAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AnalyticsContactEvent_workspaceId_adminId_occurredAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "AnalyticsContactEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "occurredAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "eventType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AnalyticsConversationEvent_workspaceId_occurredAt_eventType_idx", + "entityType": "indexes", + "schema": "public", + "table": "AnalyticsConversationEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "toAssignee", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "occurredAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AnalyticsConversationEvent_workspaceId_toAssignee_occurredAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "AnalyticsConversationEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "flowId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "analyticsId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "nodeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "occurredAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AnalyticsFlowNodeEvent_workspaceId_flowId_analyticsId_nodeId_occurredAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "AnalyticsFlowNodeEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "flowId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "analyticsId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "nodeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "buttonId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "occurredAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AnalyticsFlowNodeEvent_workspaceId_flowId_analyticsId_nodeId_buttonId_occurredAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "AnalyticsFlowNodeEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "occurredAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "eventType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AnalyticsMessageEvent_workspaceId_occurredAt_eventType_idx", + "entityType": "indexes", + "schema": "public", + "table": "AnalyticsMessageEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "eventType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "occurredAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AnalyticsMessageEvent_workspaceId_eventType_occurredAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "AnalyticsMessageEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "adminId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "occurredAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AnalyticsMessageEvent_workspaceId_adminId_occurredAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "AnalyticsMessageEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "senderType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "occurredAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AnalyticsMessageEvent_workspaceId_senderType_occurredAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "AnalyticsMessageEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "sequenceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "stepId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "eventType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "occurredAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AnalyticsSequenceEvent_workspaceId_sequenceId_stepId_eventType_occurredAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "AnalyticsSequenceEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "token", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AnalyticsEmailTopic_token_key", + "entityType": "indexes", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "topicId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AnalyticsEmailTopic_topicId_idx", + "entityType": "indexes", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "topicId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AnalyticsEmailTopic_workspaceId_topicId_idx", + "entityType": "indexes", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "objectKey", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AnalyticsManifestStatus_objectKey_key", + "entityType": "indexes", + "schema": "public", + "table": "AnalyticsManifestStatus" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Appointment_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Appointment" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "startAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Appointment_workspaceId_startAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "Appointment" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "calendarId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "startAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Appointment_calendarId_status_startAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "Appointment" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "calendarId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Appointment_contactId_calendarId_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "Appointment" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AppointmentCalendar_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "(\"deletedAt\" is null)", + "with": "", + "method": "btree", + "concurrently": false, + "name": "AppointmentCalendar_workspaceId_name_key", + "entityType": "indexes", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "publicLinkSlug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AppointmentCalendar_publicLinkSlug_key", + "entityType": "indexes", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "calendarId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AppointmentCalendarAvailability_calendarId_idx", + "entityType": "indexes", + "schema": "public", + "table": "AppointmentCalendarAvailability" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "calendarId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "flowId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "timingValue", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "timingUnit", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AppointmentCalendarReminder_dedupe_key", + "entityType": "indexes", + "schema": "public", + "table": "AppointmentCalendarReminder" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "jobId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AppointmentReminderDispatch_jobId_key", + "entityType": "indexes", + "schema": "public", + "table": "AppointmentReminderDispatch" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "sendAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AppointmentReminderDispatch_status_sendAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "AppointmentReminderDispatch" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "appointmentId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AppointmentReminderDispatch_appointmentId_idx", + "entityType": "indexes", + "schema": "public", + "table": "AppointmentReminderDispatch" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactInboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AppointmentReminderDispatch_contactInboxId_idx", + "entityType": "indexes", + "schema": "public", + "table": "AppointmentReminderDispatch" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "messageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "messageCreatedAt", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Attachment_message_idx", + "entityType": "indexes", + "schema": "public", + "table": "Attachment" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Attachment_workspaceId_createdAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "Attachment" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "conversationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Attachment_conversationId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Attachment" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "providerId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "accountId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "tenantId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Account_providerId_accountId_tenantId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Account" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "code", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Invitation_code_key", + "entityType": "indexes", + "schema": "public", + "table": "Invitation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "token", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Session_token_key", + "entityType": "indexes", + "schema": "public", + "table": "Session" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "email", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "tenantId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "User_email_tenant_key", + "entityType": "indexes", + "schema": "public", + "table": "User" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tenantId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "User_tenantId_idx", + "entityType": "indexes", + "schema": "public", + "table": "User" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AutomatedResponse_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "AutomatedResponse" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "lastTriggeredAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AutomationThrottle_lastTriggeredAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "AutomationThrottle" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "BotField_workspaceId_type_name_key", + "entityType": "indexes", + "schema": "public", + "table": "BotField" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Broadcast_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Broadcast" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "flowId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Broadcast_flowId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Broadcast" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "channel", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Broadcast_channel_idx", + "entityType": "indexes", + "schema": "public", + "table": "Broadcast" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "schedulesAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Broadcast_schedulesAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "Broadcast" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Broadcast_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "Broadcast" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "deletedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"deletedAt\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "Broadcast_deletedAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "Broadcast" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "inboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "BroadcastTarget_inboxId_idx", + "entityType": "indexes", + "schema": "public", + "table": "BroadcastTarget" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "flowId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "BroadcastTarget_flowId_idx", + "entityType": "indexes", + "schema": "public", + "table": "BroadcastTarget" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "CoexistSyncRun_workspace_idx", + "entityType": "indexes", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "CoexistSyncRun_integration_idx", + "entityType": "indexes", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "lastHeartbeatAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "status IN ('init', 'running')", + "with": "", + "method": "btree", + "concurrently": false, + "name": "CoexistSyncRun_active_idx", + "entityType": "indexes", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "channel", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "\"startedAt\" DESC", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "status IN ('succeeded', 'partial')", + "with": "", + "method": "btree", + "concurrently": false, + "name": "CoexistSyncRun_integration_resume_idx", + "entityType": "indexes", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "channel", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "status = 'init' AND type = 'coexist'", + "with": "", + "method": "btree", + "concurrently": false, + "name": "CoexistSyncRun_integration_init_uq", + "entityType": "indexes", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "type = 'contact_scan' AND status IN ('init', 'running')", + "with": "", + "method": "btree", + "concurrently": false, + "name": "CoexistSyncRun_contact_scan_active_uq", + "entityType": "indexes", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "type = 'contact_scan'", + "with": "", + "method": "btree", + "concurrently": false, + "name": "CoexistSyncRun_contact_scan_due_idx", + "entityType": "indexes", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ConnectSession_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "ConnectSession" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "expiresAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ConnectSession_expiresAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "ConnectSession" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "stateNonceHash", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ConnectSession_stateNonceHash_key", + "entityType": "indexes", + "schema": "public", + "table": "ConnectSession" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Connection_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Connection" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "provider", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "sourceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Connection_workspaceId_provider_sourceId_key", + "entityType": "indexes", + "schema": "public", + "table": "Connection" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "inboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Connection_inboxId_key", + "entityType": "indexes", + "schema": "public", + "table": "Connection" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Connection_integrationId_key", + "entityType": "indexes", + "schema": "public", + "table": "Connection" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "authExpiresAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Connection_status_authExpiresAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "Connection" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "broadcastSubscribedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_contact_broadcast_subscribed_at", + "entityType": "indexes", + "schema": "public", + "table": "Contact" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_contact_workspace_created_at", + "entityType": "indexes", + "schema": "public", + "table": "Contact" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "email", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_contact_workspace_email", + "entityType": "indexes", + "schema": "public", + "table": "Contact" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "phoneNumber", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_contact_workspace_phone_number", + "entityType": "indexes", + "schema": "public", + "table": "Contact" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "firstName", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "gin_trgm_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "Contact_firstName_trgm_idx", + "entityType": "indexes", + "schema": "public", + "table": "Contact" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "lastName", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "gin_trgm_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "Contact_lastName_trgm_idx", + "entityType": "indexes", + "schema": "public", + "table": "Contact" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "email", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "gin_trgm_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "Contact_email_trgm_idx", + "entityType": "indexes", + "schema": "public", + "table": "Contact" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "phoneNumber", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "gin_trgm_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "Contact_phoneNumber_trgm_idx", + "entityType": "indexes", + "schema": "public", + "table": "Contact" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "customFieldId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ContactCustomField_contactId_customFieldId_key", + "entityType": "indexes", + "schema": "public", + "table": "ContactCustomField" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "customFieldId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ContactCustomField_customFieldId_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "ContactCustomField" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "inboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "sourceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ContactInbox_inboxId_sourceId_key", + "entityType": "indexes", + "schema": "public", + "table": "ContactInbox" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "inboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "sourceUserId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"sourceUserId\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "ContactInbox_inboxId_sourceUserId_key", + "entityType": "indexes", + "schema": "public", + "table": "ContactInbox" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "lastIncomingMessageAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ContactInbox_contactId_lastIncomingMessageAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "ContactInbox" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "lastOutboundMessageAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ContactInbox_contactId_lastOutboundMessageAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "ContactInbox" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "(\"referral\"->>'ctwaClid')", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"referral\"->>'ctwaClid' IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "ContactInbox_referral_ctwaClid_idx", + "entityType": "indexes", + "schema": "public", + "table": "ContactInbox" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "(\"referral\"->>'adId')", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"referral\"->>'adId' IS NOT NULL AND \"referral\"->>'source' = 'ADS'", + "with": "", + "method": "btree", + "concurrently": false, + "name": "ContactInbox_referral_adId_idx", + "entityType": "indexes", + "schema": "public", + "table": "ContactInbox" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ContactNote_contactId_idx", + "entityType": "indexes", + "schema": "public", + "table": "ContactNote" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_contact_on_broadcast_contact_id", + "entityType": "indexes", + "schema": "public", + "table": "ContactOnBroadcast" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "isRead", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_contact_on_broadcast_is_read", + "entityType": "indexes", + "schema": "public", + "table": "ContactOnBroadcast" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "broadcastId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"sent\" = false AND \"failedAt\" IS NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "ContactOnBroadcast_unsent_idx", + "entityType": "indexes", + "schema": "public", + "table": "ContactOnBroadcast" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "sequenceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ContactsOnSequence_sequenceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ContactsOnSequence_contactId_idx", + "entityType": "indexes", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ContactsOnSequence_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "nextRunAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ContactsOnSequence_status_nextRunAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "nextRunAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ContactsOnSequence_workspaceId_status_nextRunAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "sequenceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ContactsOnSequence_contactId_sequenceId_workspaceId_key", + "entityType": "indexes", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "flowId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "contactInboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "stepId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ContactOnSmartDelay_workspaceId_flowId_contactInboxId_stepId_idx", + "entityType": "indexes", + "schema": "public", + "table": "ContactOnSmartDelay" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "triggerAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ContactOnSmartDelay_status_triggerAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "ContactOnSmartDelay" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "conversationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ContactOnSmartDelay_conversationId_idx", + "entityType": "indexes", + "schema": "public", + "table": "ContactOnSmartDelay" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "appointmentId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ContactOnSmartDelay_appointmentId_idx", + "entityType": "indexes", + "schema": "public", + "table": "ContactOnSmartDelay" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "contactInboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "flowId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "stepId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"status\" NOT IN ('completed', 'failed', 'canceled') AND \"type\" = 'followUp'", + "with": "", + "method": "btree", + "concurrently": false, + "name": "ContactOnSmartDelay_followUp_active_key", + "entityType": "indexes", + "schema": "public", + "table": "ContactOnSmartDelay" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactInboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ContactToTagChannel_contactInboxId_idx", + "entityType": "indexes", + "schema": "public", + "table": "ContactToTagChannel" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tagId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ContactToTagChannel_tagId_idx", + "entityType": "indexes", + "schema": "public", + "table": "ContactToTagChannel" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "sourceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"sourceId\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "Conversation_contactId_sourceId_key", + "entityType": "indexes", + "schema": "public", + "table": "Conversation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"sourceId\" IS NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "Conversation_contactId_dm_key", + "entityType": "indexes", + "schema": "public", + "table": "Conversation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "aiContextLastMessageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Conversation_aiContextLastMessageId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Conversation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "lastActivityAt", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Conversation_workspaceId_lastActivityAt_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "Conversation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ConversationParticipant_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "ConversationParticipant" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "conversationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ConversationParticipant_conversationId_idx", + "entityType": "indexes", + "schema": "public", + "table": "ConversationParticipant" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "conversationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ConversationParticipant_conversationId_userId_key", + "entityType": "indexes", + "schema": "public", + "table": "ConversationParticipant" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "code", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Coupon_workspaceId_code_key", + "entityType": "indexes", + "schema": "public", + "table": "Coupon" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "topicId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "issuedContactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"issuedContactId\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "Coupon_workspaceId_topicId_issuedContactId_key", + "entityType": "indexes", + "schema": "public", + "table": "Coupon" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "topicId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Coupon_workspaceId_topicId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Coupon" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "topicId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "issuedContactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Coupon_workspaceId_topicId_issuedContactId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Coupon" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "issuedContactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Coupon_issuedContactId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Coupon" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "issuedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Coupon_issuedAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "Coupon" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "usedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Coupon_usedAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "Coupon" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "code", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Coupon_workspaceId_code_idx", + "entityType": "indexes", + "schema": "public", + "table": "Coupon" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "topicId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "issuedContactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "usedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Coupon_workspaceId_topicId_issuedContactId_usedAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "Coupon" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "deletedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "CouponTopic_workspaceId_status_deletedAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "CouponTopic" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "expiresAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "CouponTopic_workspaceId_expiresAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "CouponTopic" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "lower(\"name\")", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"deletedAt\" IS NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "CouponTopic_workspaceId_name_idx", + "entityType": "indexes", + "schema": "public", + "table": "CouponTopic" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "CustomField_workspaceId_type_name_key", + "entityType": "indexes", + "schema": "public", + "table": "CustomField" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "DynamicImage_workspaceId_name_key", + "entityType": "indexes", + "schema": "public", + "table": "DynamicImage" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "EmailTopic_workspaceId_name_key", + "entityType": "indexes", + "schema": "public", + "table": "EmailTopic" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "folderId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "EmailTopic_folderId_idx", + "entityType": "indexes", + "schema": "public", + "table": "EmailTopic" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "AuditLog_workspaceId_createdAt_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "AuditLog" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"userId\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "AuditLog_workspaceId_userId_createdAt_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "AuditLog" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tenantId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "CustomDomain_tenantId_key", + "entityType": "indexes", + "schema": "public", + "table": "CustomDomain" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "domain", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "CustomDomain_domain_key", + "entityType": "indexes", + "schema": "public", + "table": "CustomDomain" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "CustomDomain_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "CustomDomain" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "ownerId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Tenant_ownerId_key", + "entityType": "indexes", + "schema": "public", + "table": "Tenant" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tenantId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "TenantHelpItem_tenantId_position_idx", + "entityType": "indexes", + "schema": "public", + "table": "TenantHelpItem" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "channelsTornDownAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "UserQuota_channelsTornDownAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "UserQuota" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"channelsTornDownAt\" IS NULL AND \"planStatus\" = 'trial'", + "with": "", + "method": "btree", + "concurrently": false, + "name": "UserQuota_due_expired_trial_idx", + "entityType": "indexes", + "schema": "public", + "table": "UserQuota" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": false, + "nullsFirst": true, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ErrorLog_workspaceId_createdAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "ErrorLog" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ErrorLog_createdAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "ErrorLog" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ErrorLog_contactId_idx", + "entityType": "indexes", + "schema": "public", + "table": "ErrorLog" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "event", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ExternalWebhook_workspaceId_event_idx", + "entityType": "indexes", + "schema": "public", + "table": "ExternalWebhook" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "event", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "url", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ExternalWebhook_workspaceId_event_url_key", + "entityType": "indexes", + "schema": "public", + "table": "ExternalWebhook" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "pageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "formId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "FacebookLeadAdsAutomation_workspaceId_pageId_formId_key", + "entityType": "indexes", + "schema": "public", + "table": "FacebookLeadAdsAutomation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "automationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "leadgenId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "FacebookLeadAdsLead_automationId_leadgenId_key", + "entityType": "indexes", + "schema": "public", + "table": "FacebookLeadAdsLead" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "leadgenId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "FacebookLeadAdsLead_leadgenId_idx", + "entityType": "indexes", + "schema": "public", + "table": "FacebookLeadAdsLead" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "FBCommentAutomation_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "folderId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "FBCommentAutomation_folderId_idx", + "entityType": "indexes", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "automationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "commentId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "replyChannel", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "FBCommentAutomationEvent_dedup_idx", + "entityType": "indexes", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "automationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "occurredAt", + "isExpression": false, + "asc": false, + "nullsFirst": true, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "FBCommentAutomationEvent_automation_occurredAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "FBCommentAutomationEvent_contactId_idx", + "entityType": "indexes", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactInboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "FBCommentAutomationEvent_contactInboxId_idx", + "entityType": "indexes", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactInboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"replyChannel\" = 'private' AND \"deliveredAt\" IS NOT NULL AND \"seenAt\" IS NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "FBCommentAutomationEvent_private_unseen_idx", + "entityType": "indexes", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"status\" = 'failed'", + "with": "", + "method": "btree", + "concurrently": false, + "name": "FBCommentAutomationEvent_failed_createdAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "automationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "commentId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "FBCommentAutomationMiss_dedup_idx", + "entityType": "indexes", + "schema": "public", + "table": "FBCommentAutomationMiss" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "automationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "occurredAt", + "isExpression": false, + "asc": false, + "nullsFirst": true, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "FBCommentAutomationMiss_automation_occurredAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "FBCommentAutomationMiss" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "FBCommentAutomationMiss_contactId_idx", + "entityType": "indexes", + "schema": "public", + "table": "FBCommentAutomationMiss" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactInboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "FBCommentAutomationMiss_contactInboxId_idx", + "entityType": "indexes", + "schema": "public", + "table": "FBCommentAutomationMiss" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "automationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "postId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "FBCommentAutomationReply_dedup_idx", + "entityType": "indexes", + "schema": "public", + "table": "FBCommentAutomationReply" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "FBCommentAutomationReply_contactId_idx", + "entityType": "indexes", + "schema": "public", + "table": "FBCommentAutomationReply" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "path", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "File_path_key", + "entityType": "indexes", + "schema": "public", + "table": "File" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "contextType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "File_workspaceId_contextType_idx", + "entityType": "indexes", + "schema": "public", + "table": "File" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "subType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "File_workspaceId_subType_idx", + "entityType": "indexes", + "schema": "public", + "table": "File" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "flowId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"deletedAt\" IS NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "FlowAnalyticsSession_workspaceId_flowId_key", + "entityType": "indexes", + "schema": "public", + "table": "FlowAnalyticsSession" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "flowId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "FlowAnalyticsSession_flowId_idx", + "entityType": "indexes", + "schema": "public", + "table": "FlowAnalyticsSession" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "analyticsId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "nodeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "eventType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "buttonId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "FlowNodeStat_filter_1_idx", + "entityType": "indexes", + "schema": "public", + "table": "FlowNodeStat" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "analyticsId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "nodeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"eventType\" = 'seen' AND \"seenAt\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "FlowNodeStat_filter_2_idx", + "entityType": "indexes", + "schema": "public", + "table": "FlowNodeStat" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "conversationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "FlowRun_conversationId_idx", + "entityType": "indexes", + "schema": "public", + "table": "FlowRun" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Folder_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Folder" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "parentId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Folder_parentId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Folder" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IgStoryAutomation_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "IgStoryAutomation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "folderId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IgStoryAutomation_folderId_idx", + "entityType": "indexes", + "schema": "public", + "table": "IgStoryAutomation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Import_workspaceId_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "Import" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Import_workspaceId_type_idx", + "entityType": "indexes", + "schema": "public", + "table": "Import" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "inboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Import_inboxId_type_idx", + "entityType": "indexes", + "schema": "public", + "table": "Import" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "fileId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Import_fileId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Import" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"type\" = 'products' AND \"status\" IN ('pending', 'processing')", + "with": "", + "method": "btree", + "concurrently": false, + "name": "Import_products_active_idx", + "entityType": "indexes", + "schema": "public", + "table": "Import" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Inbox_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Inbox" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "channel", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "sourceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Inbox_workspaceId_channel_sourceId_key", + "entityType": "indexes", + "schema": "public", + "table": "Inbox" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationActiveCampaign_integrationId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationActiveCampaign" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationActiveCampaign_workspaceId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationActiveCampaign" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationApi_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationApi" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "inboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationApi_inboxId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationApi" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tokenHash", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationApi_tokenHash_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationApi" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Integration_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Integration" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "integrationType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Integration_workspaceId_integrationType_key", + "entityType": "indexes", + "schema": "public", + "table": "Integration" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationClaude_workspaceId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationClaude" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationClaude_integrationId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationClaude" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationDeepseek_workspaceId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationDeepseek" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationDeepseek_integrationId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationDeepseek" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationDrip_integrationId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationDrip" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationDrip_workspaceId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationDrip" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationFacebookAds_integrationId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationFacebookAds" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationFacebookAds_workspaceId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationFacebookAds" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationGemini_workspaceId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationGemini" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationGemini_integrationId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationGemini" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationGetResponse_integrationId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationGetResponse" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationGetResponse_workspaceId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationGetResponse" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationGoogleCalendar_integrationId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationGoogleCalendar" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationGoogleSheet_integrationId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationGoogleSheet" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationInstagram_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "welcomeFlowId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationInstagram_welcomeFlowId_idx", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "inboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationInstagram_inboxId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "igId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationInstagram_igId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationKlaviyo_workspaceId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationKlaviyo" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationKlaviyo_integrationId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationKlaviyo" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationMailchimp_integrationId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationMailchimp" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationMailchimp_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationMailchimp" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationMailerLite_workspaceId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationMailerLite" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationMailerLite_integrationId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationMailerLite" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationMessenger_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "welcomeFlowId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationMessenger_welcomeFlowId_idx", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "inboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationMessenger_inboxId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "pageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationMessenger_pageId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationMetaCatalog_integrationId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationMetaCatalog_workspaceId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "deletedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationMetaCatalog_deletedAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationMoosend_workspaceId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationMoosend" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationMoosend_integrationId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationMoosend" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationOpenAI_integrationId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationOpenai" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationOpenaiCompatible_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationOpenaiCompatible" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationOpenaiCompatible_integrationId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationOpenaiCompatible" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "preset", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"preset\" <> 'custom'", + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationOpenaiCompatible_workspaceId_preset_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationOpenaiCompatible" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationOpenrouter_workspaceId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationOpenrouter" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationOpenrouter_integrationId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationOpenrouter" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationOutlookCalendar_integrationId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationOutlookCalendar" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationSendGrid_integrationId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationSendGrid" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationSendGrid_workspaceId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationSendGrid" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationSmtp_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationSmtp" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "inboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationSmtp_inboxId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationSmtp" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationTelegram_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationTelegram" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "inboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationTelegram_inboxId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationTelegram" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "botId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationTelegram_botId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationTelegram" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationTiktok_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationTiktok" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "inboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationTiktok_inboxId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationTiktok" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "openId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationTiktok_openId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationTiktok" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationWebchat_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationWebchat" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "inboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationWebchat_inboxId_idx", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationWebchat" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "inboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationWebchat_inboxId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationWebchat" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "welcomeFlowId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationWebchat_welcomeFlowId_idx", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationWebchat" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "inboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationWhatsapp_inboxId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationWhatsapp_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "phoneNumberId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationWhatsapp_phoneNumberId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationZalo_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationZalo" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "fallbackFlowId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationZalo_fallbackFlowId_idx", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationZalo" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "inboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "IntegrationZalo_inboxId_key", + "entityType": "indexes", + "schema": "public", + "table": "IntegrationZalo" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MagicLink_workspaceId_name_key", + "entityType": "indexes", + "schema": "public", + "table": "MagicLink" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MagicLink_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "MagicLink" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "linkId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "occurredAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "contactInboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MagicLinkStat_workspaceId_linkId_occurredAt_contactInboxId_key", + "entityType": "indexes", + "schema": "public", + "table": "MagicLinkStat" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MediaLibraryFile_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "MediaLibraryFile" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "folderId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MediaLibraryFile_folderId_idx", + "entityType": "indexes", + "schema": "public", + "table": "MediaLibraryFile" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MediaLibraryFolder_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "MediaLibraryFolder" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "conversationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Message_conversation_history_idx", + "entityType": "indexes", + "schema": "public", + "table": "Message" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Message_workspace_created_idx", + "entityType": "indexes", + "schema": "public", + "table": "Message" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactInboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "sourceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Message_contactInboxId_sourceId_createdAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "Message" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "conversationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Message_conversationId_type_idx", + "entityType": "indexes", + "schema": "public", + "table": "Message" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "parentId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"parentId\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "Message_parentId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Message" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "inboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "sourceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MessageCleanup_inboxId_sourceId_key", + "entityType": "indexes", + "schema": "public", + "table": "MessageCleanup" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MessageCleanup_status_createdAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "MessageCleanup" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MessageCleanup_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "MessageCleanup" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MessagingAdOperation_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationWhatsappId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MessagingAdOperation_integrationWhatsappId_idx", + "entityType": "indexes", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationMessengerId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MessagingAdOperation_integrationMessengerId_idx", + "entityType": "indexes", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationInstagramId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MessagingAdOperation_integrationInstagramId_idx", + "entityType": "indexes", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MessagingAdsConnection_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "MessagingAdsConnection" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationWhatsappId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MessagingAdsConnection_integrationWhatsappId_key", + "entityType": "indexes", + "schema": "public", + "table": "MessagingAdsConnection" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationMessengerId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MessagingAdsConnection_integrationMessengerId_key", + "entityType": "indexes", + "schema": "public", + "table": "MessagingAdsConnection" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationInstagramId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MessagingAdsConnection_integrationInstagramId_key", + "entityType": "indexes", + "schema": "public", + "table": "MessagingAdsConnection" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationMessengerId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "sourceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MessengerMessageTemplate_integrationMessengerId_sourceId_key", + "entityType": "indexes", + "schema": "public", + "table": "MessengerMessageTemplate" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "channel", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "sourceKey", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MetaCapiEvent_workspaceId_channel_sourceKey_key", + "entityType": "indexes", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactInboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MetaCapiEvent_contactInboxId_idx", + "entityType": "indexes", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "channel", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MetaCapiEvent_channel_integrationId_idx", + "entityType": "indexes", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationMetaCatalogId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "catalogId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "retailerId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MetaCatalogItem_integration_retailer_key", + "entityType": "indexes", + "schema": "public", + "table": "MetaCatalogItem" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationMetaCatalogId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "catalogId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "productId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MetaCatalogItem_integration_product_key", + "entityType": "indexes", + "schema": "public", + "table": "MetaCatalogItem" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "productId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MetaCatalogItem_productId_idx", + "entityType": "indexes", + "schema": "public", + "table": "MetaCatalogItem" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"status\" IN ('queued', 'running')", + "with": "", + "method": "btree", + "concurrently": false, + "name": "MetaCatalogSyncRun_active_idx", + "entityType": "indexes", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Minigame_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Minigame" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Minigame_workspaceId_name_key", + "entityType": "indexes", + "schema": "public", + "table": "Minigame" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "minigameId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MinigameContact_minigameId_idx", + "entityType": "indexes", + "schema": "public", + "table": "MinigameContact" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MinigameContact_contactId_idx", + "entityType": "indexes", + "schema": "public", + "table": "MinigameContact" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "minigameId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MinigameContact_minigameId_contactId_key", + "entityType": "indexes", + "schema": "public", + "table": "MinigameContact" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "minigameId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MinigamePlay_minigameId_contactId_idx", + "entityType": "indexes", + "schema": "public", + "table": "MinigamePlay" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "MinigamePlay_contactId_idx", + "entityType": "indexes", + "schema": "public", + "table": "MinigamePlay" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "livemode", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"userId\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "PlatformCredential_user_type_livemode_key", + "entityType": "indexes", + "schema": "public", + "table": "PlatformCredential" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "livemode", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"userId\" IS NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "PlatformCredential_platform_type_livemode_key", + "entityType": "indexes", + "schema": "public", + "table": "PlatformCredential" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "PlatformCredential_userId_idx", + "entityType": "indexes", + "schema": "public", + "table": "PlatformCredential" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Product_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Product" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "categoryId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Product_categoryId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Product" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "subcategoryId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Product_subcategoryId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Product" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "productId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ProductAddon_productId_idx", + "entityType": "indexes", + "schema": "public", + "table": "ProductAddon" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ProductCategory_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "ProductCategory" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "parentId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ProductCategory_parentId_idx", + "entityType": "indexes", + "schema": "public", + "table": "ProductCategory" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "productId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ProductVariant_productId_idx", + "entityType": "indexes", + "schema": "public", + "table": "ProductVariant" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "productId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "ProductVariantOption_productId_idx", + "entityType": "indexes", + "schema": "public", + "table": "ProductVariantOption" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "submissionId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "questionIdSnapshot", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "QuestionnaireAnswer_submissionId_questionIdSnapshot_key", + "entityType": "indexes", + "schema": "public", + "table": "QuestionnaireAnswer" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "questionId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "QuestionnaireAnswer_questionId_idx", + "entityType": "indexes", + "schema": "public", + "table": "QuestionnaireAnswer" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Questionnaire_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Questionnaire" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "(\"deletedAt\" is null)", + "with": "", + "method": "btree", + "concurrently": false, + "name": "Questionnaire_workspaceId_name_key", + "entityType": "indexes", + "schema": "public", + "table": "Questionnaire" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "questionnaireId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "orderNo", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "QuestionnaireQuestion_questionnaireId_orderNo_idx", + "entityType": "indexes", + "schema": "public", + "table": "QuestionnaireQuestion" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "customFieldId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "QuestionnaireQuestion_customFieldId_idx", + "entityType": "indexes", + "schema": "public", + "table": "QuestionnaireQuestion" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "QuestionnaireSubmission_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "QuestionnaireSubmission" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "questionnaireId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "QuestionnaireSubmission_questionnaireId_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "QuestionnaireSubmission" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "questionnaireId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "QuestionnaireSubmission_questionnaireId_contactId_idx", + "entityType": "indexes", + "schema": "public", + "table": "QuestionnaireSubmission" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"status\" = 'inProgress'", + "with": "", + "method": "btree", + "concurrently": false, + "name": "QuestionnaireSubmission_workspaceId_contactId_active_key", + "entityType": "indexes", + "schema": "public", + "table": "QuestionnaireSubmission" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "RefLinkStat_contactId_idx", + "entityType": "indexes", + "schema": "public", + "table": "RefLinkStat" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "linkId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "occurredAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "contactInboxId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "RefLinkStat_workspaceId_linkId_occurredAt_contactInboxId_key", + "entityType": "indexes", + "schema": "public", + "table": "RefLinkStat" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Reflink_workspaceId_name_key", + "entityType": "indexes", + "schema": "public", + "table": "Reflink" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "folderId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Sequence_folderId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Sequence" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Sequence_workspaceId_name_key", + "entityType": "indexes", + "schema": "public", + "table": "Sequence" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "idempotencyKey", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "SequenceDispatch_idempotencyKey_key", + "entityType": "indexes", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "SequenceDispatch_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "runAtMs", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"status\" = 'pending'", + "with": "", + "method": "btree", + "concurrently": false, + "name": "SequenceDispatch_pending_runAtMs_idx", + "entityType": "indexes", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "bucket", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "runAtMs", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"status\" = 'pending'", + "with": "", + "method": "btree", + "concurrently": false, + "name": "SequenceDispatch_pending_bucket_runAtMs_idx", + "entityType": "indexes", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "runAtMs", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "SequenceDispatch_status_runAtMs_idx", + "entityType": "indexes", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "runAtMs", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "SequenceDispatch_workspaceId_status_runAtMs_idx", + "entityType": "indexes", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "enrollmentId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "SequenceDispatch_enrollmentId_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "sequenceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "stepId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "SequenceDispatch_workspace_sequence_step_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "bucket", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "runAtMs", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "SequenceDispatch_bucket_status_runAtMs_idx", + "entityType": "indexes", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"status\" in ('completed', 'failed', 'canceled')", + "with": "", + "method": "btree", + "concurrently": false, + "name": "SequenceDispatch_terminal_updatedAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "SequenceDispatch_contactId_idx", + "entityType": "indexes", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "sequenceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "SequenceStep_sequenceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "SequenceStep" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "flowId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "SequenceStep_flowId_idx", + "entityType": "indexes", + "schema": "public", + "table": "SequenceStep" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Spreadsheet_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Spreadsheet" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "spreadsheetId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Spreadsheet_workspaceId_spreadsheetId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Spreadsheet" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "spreadsheetId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Spreadsheet_spreadsheetId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Spreadsheet" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "(\"deletedAt\" is null)", + "with": "", + "method": "btree", + "concurrently": false, + "name": "Tag_workspaceId_name_key", + "entityType": "indexes", + "schema": "public", + "table": "Tag" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "folderId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Tag_folderId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Tag" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tagId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "channelType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "TagChannel_tag_integration_key", + "entityType": "indexes", + "schema": "public", + "table": "TagChannel" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "channelType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "externalLabelId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "TagChannel_external_key", + "entityType": "indexes", + "schema": "public", + "table": "TagChannel" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "channelType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "TagChannel_workspace_channel_idx", + "entityType": "indexes", + "schema": "public", + "table": "TagChannel" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "channelType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "integrationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "TagChannel_integration_idx", + "entityType": "indexes", + "schema": "public", + "table": "TagChannel" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Template_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Template" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tenantId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Template_tenantId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Template" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "shareToken", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Template_shareToken_key", + "entityType": "indexes", + "schema": "public", + "table": "Template" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "TemplateInstallation_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "templateId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "TemplateInstallation_templateId_idx", + "entityType": "indexes", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "TemplateInstallation_workspaceId_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "installationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "TemplateInstalledResource_installationId_idx", + "entityType": "indexes", + "schema": "public", + "table": "TemplateInstalledResource" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "resourceKind", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "resourceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "TemplateInstalledResource_workspaceId_resourceKind_resourceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "TemplateInstalledResource" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Trigger_workspaceId_name_key", + "entityType": "indexes", + "schema": "public", + "table": "Trigger" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Trigger_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Trigger" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "folderId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Trigger_folderId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Trigger" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "active", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Trigger_workspaceId_active_idx", + "entityType": "indexes", + "schema": "public", + "table": "Trigger" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "sourceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Condition_type_source_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "Condition" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "triggerId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Condition_triggerId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Condition" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "webhookId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Condition_webhookId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Condition" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "sourceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "triggerId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Condition_type_sourceId_triggerId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Condition" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "sourceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "webhookId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Condition_type_sourceId_webhookId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Condition" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "triggerId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "TriggerContactHistory_triggerId_contactId_idx", + "entityType": "indexes", + "schema": "public", + "table": "TriggerContactHistory" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "TriggerContactHistory_contactId_idx", + "entityType": "indexes", + "schema": "public", + "table": "TriggerContactHistory" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "TriggerContactHistory_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "TriggerContactHistory" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "triggerId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "TriggerExecution_triggerId_contactId_idx", + "entityType": "indexes", + "schema": "public", + "table": "TriggerExecution" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "TriggerExecution_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "TriggerExecution" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "TriggerExecution_contactId_idx", + "entityType": "indexes", + "schema": "public", + "table": "TriggerExecution" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "triggerId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "date", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "TriggerStat_triggerId_date_key", + "entityType": "indexes", + "schema": "public", + "table": "TriggerStat" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "triggerId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "date", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "TriggerStat_triggerId_date_idx", + "entityType": "indexes", + "schema": "public", + "table": "TriggerStat" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "date", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "TriggerStat_workspaceId_date_idx", + "entityType": "indexes", + "schema": "public", + "table": "TriggerStat" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "UserDeviceToken_userId_idx", + "entityType": "indexes", + "schema": "public", + "table": "UserDeviceToken" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "UserPersistentMenu_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "UserPersistentMenu" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Webhook_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Webhook" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "folderId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Webhook_folderId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Webhook" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "active", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Webhook_workspaceId_active_idx", + "entityType": "indexes", + "schema": "public", + "table": "Webhook" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "webhookId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "WebhookExecution_webhookId_contactId_key", + "entityType": "indexes", + "schema": "public", + "table": "WebhookExecution" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "WebhookExecution_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "WebhookExecution" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contactId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "WebhookExecution_contactId_idx", + "entityType": "indexes", + "schema": "public", + "table": "WebhookExecution" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "wabaId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "WhatsappBusinessAccount_workspaceId_wabaId_key", + "entityType": "indexes", + "schema": "public", + "table": "WhatsappBusinessAccount" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "phoneNumberId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "WhatsappCoexistStaging_phoneNumberId_idx", + "entityType": "indexes", + "schema": "public", + "table": "WhatsappCoexistStaging" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "phoneNumberId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "payloadHash", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "WhatsappCoexistStaging_phone_hash_uq", + "entityType": "indexes", + "schema": "public", + "table": "WhatsappCoexistStaging" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "processedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"processedAt\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "WhatsappCoexistStaging_processedAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "WhatsappCoexistStaging" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "parseFailedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"parseFailedAt\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "WhatsappCoexistStaging_parseFailedAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "WhatsappCoexistStaging" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationWhatsappId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "sourceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "WhatsappFlow_integrationWhatsappId_sourceId_key", + "entityType": "indexes", + "schema": "public", + "table": "WhatsappFlow" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "integrationWhatsappId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "sourceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "WhatsappMessageTemplate_integrationWhatsappId_sourceId_key", + "entityType": "indexes", + "schema": "public", + "table": "WhatsappMessageTemplate" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "WhatsappSignupSession_userId_idx", + "entityType": "indexes", + "schema": "public", + "table": "WhatsappSignupSession" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "expiresAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "WhatsappSignupSession_expiresAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "WhatsappSignupSession" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tenantId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Workspace_tenantId_idx", + "entityType": "indexes", + "schema": "public", + "table": "Workspace" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "scheduledDeletionAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Workspace_scheduledDeletionAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "Workspace" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "supportAccessUntil", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "Workspace_supportAccessUntil_idx", + "entityType": "indexes", + "schema": "public", + "table": "Workspace" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "WorkspaceApiToken_workspaceId_idx", + "entityType": "indexes", + "schema": "public", + "table": "WorkspaceApiToken" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "workspaceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"isDefault\"", + "with": "", + "method": "btree", + "concurrently": false, + "name": "WorkspaceApiToken_workspaceId_default_key", + "entityType": "indexes", + "schema": "public", + "table": "WorkspaceApiToken" + }, + { + "nameExplicit": false, + "columns": [ + "shardId" + ], + "schemaTo": "public", + "tableTo": "MessageShard", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ShardTimeRange_shardId_MessageShard_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ShardTimeRange" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AdsConversionEvent_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "nameExplicit": false, + "columns": [ + "integrationWhatsappId" + ], + "schemaTo": "public", + "tableTo": "IntegrationWhatsapp", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AdsConversionEvent_IENNsXLBb1k1_fkey", + "entityType": "fks", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "nameExplicit": false, + "columns": [ + "integrationMessengerId" + ], + "schemaTo": "public", + "tableTo": "IntegrationMessenger", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AdsConversionEvent_yeTDEtvJMs4H_fkey", + "entityType": "fks", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "nameExplicit": false, + "columns": [ + "integrationInstagramId" + ], + "schemaTo": "public", + "tableTo": "IntegrationInstagram", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AdsConversionEvent_1X1qUqZpm24x_fkey", + "entityType": "fks", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "nameExplicit": false, + "columns": [ + "contactInboxId" + ], + "schemaTo": "public", + "tableTo": "ContactInbox", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "AdsConversionEvent_contactInboxId_ContactInbox_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AdsConversionRule_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AdsConversionRule" + }, + { + "nameExplicit": false, + "columns": [ + "integrationWhatsappId" + ], + "schemaTo": "public", + "tableTo": "IntegrationWhatsapp", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AdsConversionRule_k99JXbMObQIn_fkey", + "entityType": "fks", + "schema": "public", + "table": "AdsConversionRule" + }, + { + "nameExplicit": false, + "columns": [ + "integrationFacebookAdsId" + ], + "schemaTo": "public", + "tableTo": "IntegrationFacebookAds", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AdsConversionRule_fBmu8W26Mw5R_fkey", + "entityType": "fks", + "schema": "public", + "table": "AdsConversionRule" + }, + { + "nameExplicit": false, + "columns": [ + "integrationMessengerId" + ], + "schemaTo": "public", + "tableTo": "IntegrationMessenger", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AdsConversionRule_3zzAptVRRuZD_fkey", + "entityType": "fks", + "schema": "public", + "table": "AdsConversionRule" + }, + { + "nameExplicit": false, + "columns": [ + "integrationInstagramId" + ], + "schemaTo": "public", + "tableTo": "IntegrationInstagram", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AdsConversionRule_HUibBtSUZbML_fkey", + "entityType": "fks", + "schema": "public", + "table": "AdsConversionRule" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AIAgent_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AIAgent" + }, + { + "nameExplicit": false, + "columns": [ + "sourceId" + ], + "schemaTo": "public", + "tableTo": "AIConversationSource", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AIConversationEmbedding_sourceId_AIConversationSource_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AIConversationEmbedding" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AIConversationEmbedding_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AIConversationEmbedding" + }, + { + "nameExplicit": false, + "columns": [ + "conversationId" + ], + "schemaTo": "public", + "tableTo": "Conversation", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AIConversationEmbedding_conversationId_Conversation_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AIConversationEmbedding" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AIConversationSource_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AIConversationSource" + }, + { + "nameExplicit": false, + "columns": [ + "conversationId" + ], + "schemaTo": "public", + "tableTo": "Conversation", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AIConversationSource_conversationId_Conversation_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AIConversationSource" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AIEmbedding_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AIEmbedding" + }, + { + "nameExplicit": false, + "columns": [ + "aiFileId" + ], + "schemaTo": "public", + "tableTo": "AIFile", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AIEmbedding_aiFileId_AIFile_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AIEmbedding" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AIFile_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AIFile" + }, + { + "nameExplicit": false, + "columns": [ + "triggerFlowId" + ], + "schemaTo": "public", + "tableTo": "Flow", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "AIFunction_triggerFlowId_Flow_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AIFunction" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AIFunction_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AIFunction" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AIMCPServer_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AIMCPServer" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "AnalyticsBotMessageEvent_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AnalyticsBotMessageEvent" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "AnalyticsBroadcastEvent_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AnalyticsBroadcastEvent" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "AnalyticsContactEvent_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AnalyticsContactEvent" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "AnalyticsConversationEvent_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AnalyticsConversationEvent" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "AnalyticsFlowNodeEvent_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AnalyticsFlowNodeEvent" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "AnalyticsMessageEvent_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AnalyticsMessageEvent" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "AnalyticsSequenceEvent_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AnalyticsSequenceEvent" + }, + { + "nameExplicit": false, + "columns": [ + "topicId" + ], + "schemaTo": "public", + "tableTo": "EmailTopic", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AnalyticsEmailTopic_topicId_EmailTopic_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AnalyticsEmailTopic_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "nameExplicit": false, + "columns": [ + "contactId" + ], + "schemaTo": "public", + "tableTo": "Contact", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "AnalyticsEmailTopic_contactId_Contact_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "nameExplicit": false, + "columns": [ + "conversationId" + ], + "schemaTo": "public", + "tableTo": "Conversation", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "AnalyticsEmailTopic_conversationId_Conversation_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "nameExplicit": false, + "columns": [ + "contactInboxId" + ], + "schemaTo": "public", + "tableTo": "ContactInbox", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "AnalyticsEmailTopic_contactInboxId_ContactInbox_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AnalyticsEmailTopic" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Appointment_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Appointment" + }, + { + "nameExplicit": false, + "columns": [ + "calendarId" + ], + "schemaTo": "public", + "tableTo": "AppointmentCalendar", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "Appointment_calendarId_AppointmentCalendar_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Appointment" + }, + { + "nameExplicit": false, + "columns": [ + "contactId" + ], + "schemaTo": "public", + "tableTo": "Contact", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Appointment_contactId_Contact_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Appointment" + }, + { + "nameExplicit": false, + "columns": [ + "conversationId" + ], + "schemaTo": "public", + "tableTo": "Conversation", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "Appointment_conversationId_Conversation_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Appointment" + }, + { + "nameExplicit": false, + "columns": [ + "contactInboxId" + ], + "schemaTo": "public", + "tableTo": "ContactInbox", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "Appointment_contactInboxId_ContactInbox_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Appointment" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AppointmentCalendar_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "nameExplicit": false, + "columns": [ + "confirmationFlowId" + ], + "schemaTo": "public", + "tableTo": "Flow", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "AppointmentCalendar_confirmationFlowId_Flow_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "nameExplicit": false, + "columns": [ + "cancellationFlowId" + ], + "schemaTo": "public", + "tableTo": "Flow", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "AppointmentCalendar_cancellationFlowId_Flow_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "nameExplicit": false, + "columns": [ + "externalConnectionId" + ], + "schemaTo": "public", + "tableTo": "Integration", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "AppointmentCalendar_externalConnectionId_Integration_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AppointmentCalendar" + }, + { + "nameExplicit": false, + "columns": [ + "calendarId" + ], + "schemaTo": "public", + "tableTo": "AppointmentCalendar", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AppointmentCalendarAvailability_QfahoIQAX592_fkey", + "entityType": "fks", + "schema": "public", + "table": "AppointmentCalendarAvailability" + }, + { + "nameExplicit": false, + "columns": [ + "calendarId" + ], + "schemaTo": "public", + "tableTo": "AppointmentCalendar", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AppointmentCalendarReminder_5O4INfYC3dY3_fkey", + "entityType": "fks", + "schema": "public", + "table": "AppointmentCalendarReminder" + }, + { + "nameExplicit": false, + "columns": [ + "flowId" + ], + "schemaTo": "public", + "tableTo": "Flow", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AppointmentCalendarReminder_flowId_Flow_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AppointmentCalendarReminder" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AppointmentReminderDispatch_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AppointmentReminderDispatch" + }, + { + "nameExplicit": false, + "columns": [ + "appointmentId" + ], + "schemaTo": "public", + "tableTo": "Appointment", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AppointmentReminderDispatch_appointmentId_Appointment_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AppointmentReminderDispatch" + }, + { + "nameExplicit": false, + "columns": [ + "reminderConfigId" + ], + "schemaTo": "public", + "tableTo": "AppointmentCalendarReminder", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AppointmentReminderDispatch_a526jJM55OD7_fkey", + "entityType": "fks", + "schema": "public", + "table": "AppointmentReminderDispatch" + }, + { + "nameExplicit": false, + "columns": [ + "contactInboxId" + ], + "schemaTo": "public", + "tableTo": "ContactInbox", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "AppointmentReminderDispatch_contactInboxId_ContactInbox_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AppointmentReminderDispatch" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "User", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Account_userId_User_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Account" + }, + { + "nameExplicit": false, + "columns": [ + "tenantId" + ], + "schemaTo": "public", + "tableTo": "Tenant", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "Account_tenantId_Tenant_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Account" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Invitation_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Invitation" + }, + { + "nameExplicit": false, + "columns": [ + "invitedBy" + ], + "schemaTo": "public", + "tableTo": "User", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Invitation_invitedBy_User_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Invitation" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "User", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Session_userId_User_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Session" + }, + { + "nameExplicit": false, + "columns": [ + "tenantId" + ], + "schemaTo": "public", + "tableTo": "Tenant", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "User_tenantId_Tenant_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "User" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AutomatedResponse_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AutomatedResponse" + }, + { + "nameExplicit": false, + "columns": [ + "folderId" + ], + "schemaTo": "public", + "tableTo": "Folder", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "AutomatedResponse_folderId_Folder_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AutomatedResponse" + }, + { + "nameExplicit": false, + "columns": [ + "flowId" + ], + "schemaTo": "public", + "tableTo": "Flow", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "AutomatedResponse_flowId_Flow_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AutomatedResponse" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AutomationThrottle_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AutomationThrottle" + }, + { + "nameExplicit": false, + "columns": [ + "contactInboxId" + ], + "schemaTo": "public", + "tableTo": "ContactInbox", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AutomationThrottle_contactInboxId_ContactInbox_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "AutomationThrottle" + }, + { + "nameExplicit": false, + "columns": [ + "folderId" + ], + "schemaTo": "public", + "tableTo": "Folder", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "BotField_folderId_Folder_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "BotField" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "BotField_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "BotField" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Broadcast_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Broadcast" + }, + { + "nameExplicit": false, + "columns": [ + "flowId" + ], + "schemaTo": "public", + "tableTo": "Flow", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Broadcast_flowId_Flow_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Broadcast" + }, + { + "nameExplicit": false, + "columns": [ + "integrationWhatsappId" + ], + "schemaTo": "public", + "tableTo": "IntegrationWhatsapp", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "Broadcast_integrationWhatsappId_IntegrationWhatsapp_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Broadcast" + }, + { + "nameExplicit": false, + "columns": [ + "integrationMessengerId" + ], + "schemaTo": "public", + "tableTo": "IntegrationMessenger", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "Broadcast_integrationMessengerId_IntegrationMessenger_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Broadcast" + }, + { + "nameExplicit": false, + "columns": [ + "broadcastId" + ], + "schemaTo": "public", + "tableTo": "Broadcast", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "BroadcastTarget_broadcastId_Broadcast_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "BroadcastTarget" + }, + { + "nameExplicit": false, + "columns": [ + "inboxId" + ], + "schemaTo": "public", + "tableTo": "Inbox", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "BroadcastTarget_inboxId_Inbox_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "BroadcastTarget" + }, + { + "nameExplicit": false, + "columns": [ + "flowId" + ], + "schemaTo": "public", + "tableTo": "Flow", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "BroadcastTarget_flowId_Flow_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "BroadcastTarget" + }, + { + "nameExplicit": false, + "columns": [ + "requestedByUserId" + ], + "schemaTo": "public", + "tableTo": "User", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "CoexistSyncRun_requestedByUserId_User_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "CoexistSyncRun" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ConnectSession_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ConnectSession" + }, + { + "nameExplicit": false, + "columns": [ + "targetConnectionId" + ], + "schemaTo": "public", + "tableTo": "Connection", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "ConnectSession_targetConnectionId_Connection_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ConnectSession" + }, + { + "nameExplicit": false, + "columns": [ + "actorUserId" + ], + "schemaTo": "public", + "tableTo": "User", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "ConnectSession_actorUserId_User_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ConnectSession" + }, + { + "nameExplicit": false, + "columns": [ + "actorTokenId" + ], + "schemaTo": "public", + "tableTo": "WorkspaceApiToken", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "ConnectSession_actorTokenId_WorkspaceApiToken_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ConnectSession" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Connection_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Connection" + }, + { + "nameExplicit": false, + "columns": [ + "inboxId" + ], + "schemaTo": "public", + "tableTo": "Inbox", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Connection_inboxId_Inbox_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Connection" + }, + { + "nameExplicit": false, + "columns": [ + "integrationId" + ], + "schemaTo": "public", + "tableTo": "Integration", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "Connection_integrationId_Integration_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Connection" + }, + { + "nameExplicit": false, + "columns": [ + "createdBy" + ], + "schemaTo": "public", + "tableTo": "User", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "Connection_createdBy_User_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Connection" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Contact_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Contact" + }, + { + "nameExplicit": false, + "columns": [ + "contactId" + ], + "schemaTo": "public", + "tableTo": "Contact", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ContactCustomField_contactId_Contact_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ContactCustomField" + }, + { + "nameExplicit": false, + "columns": [ + "customFieldId" + ], + "schemaTo": "public", + "tableTo": "CustomField", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ContactCustomField_customFieldId_CustomField_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ContactCustomField" + }, + { + "nameExplicit": false, + "columns": [ + "contactId" + ], + "schemaTo": "public", + "tableTo": "Contact", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ContactInbox_contactId_Contact_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ContactInbox" + }, + { + "nameExplicit": false, + "columns": [ + "inboxId" + ], + "schemaTo": "public", + "tableTo": "Inbox", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ContactInbox_inboxId_Inbox_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ContactInbox" + }, + { + "nameExplicit": false, + "columns": [ + "contactId" + ], + "schemaTo": "public", + "tableTo": "Contact", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ContactNote_contactId_Contact_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ContactNote" + }, + { + "nameExplicit": false, + "columns": [ + "createdById" + ], + "schemaTo": "public", + "tableTo": "User", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ContactNote_createdById_User_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ContactNote" + }, + { + "nameExplicit": false, + "columns": [ + "broadcastId" + ], + "schemaTo": "public", + "tableTo": "Broadcast", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ContactOnBroadcast_broadcastId_Broadcast_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ContactOnBroadcast" + }, + { + "nameExplicit": false, + "columns": [ + "contactId" + ], + "schemaTo": "public", + "tableTo": "Contact", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ContactOnBroadcast_contactId_Contact_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ContactOnBroadcast" + }, + { + "nameExplicit": false, + "columns": [ + "contactInboxId" + ], + "schemaTo": "public", + "tableTo": "ContactInbox", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ContactOnBroadcast_contactInboxId_ContactInbox_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ContactOnBroadcast" + }, + { + "nameExplicit": false, + "columns": [ + "conversationId" + ], + "schemaTo": "public", + "tableTo": "Conversation", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ContactOnBroadcast_conversationId_Conversation_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ContactOnBroadcast" + }, + { + "nameExplicit": false, + "columns": [ + "contactId" + ], + "schemaTo": "public", + "tableTo": "Contact", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ContactOnSequence_contactId_Contact_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "nameExplicit": false, + "columns": [ + "sequenceId" + ], + "schemaTo": "public", + "tableTo": "Sequence", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ContactOnSequence_sequenceId_Sequence_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ContactOnSequence_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ContactOnSmartDelay_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ContactOnSmartDelay" + }, + { + "nameExplicit": false, + "columns": [ + "appointmentId" + ], + "schemaTo": "public", + "tableTo": "Appointment", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "ContactOnSmartDelay_appointmentId_Appointment_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ContactOnSmartDelay" + }, + { + "nameExplicit": false, + "columns": [ + "conversationId" + ], + "schemaTo": "public", + "tableTo": "Conversation", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ContactOnSmartDelay_conversationId_Conversation_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ContactOnSmartDelay" + }, + { + "nameExplicit": false, + "columns": [ + "contactId" + ], + "schemaTo": "public", + "tableTo": "Contact", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ContactToTag_contactId_Contact_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ContactToTag" + }, + { + "nameExplicit": false, + "columns": [ + "tagId" + ], + "schemaTo": "public", + "tableTo": "Tag", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ContactToTag_tagId_Tag_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ContactToTag" + }, + { + "nameExplicit": false, + "columns": [ + "tagId" + ], + "schemaTo": "public", + "tableTo": "Tag", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ContactToTagChannel_tagId_Tag_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ContactToTagChannel" + }, + { + "nameExplicit": false, + "columns": [ + "tagChannelId" + ], + "schemaTo": "public", + "tableTo": "TagChannel", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ContactToTagChannel_tagChannelId_TagChannel_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ContactToTagChannel" + }, + { + "nameExplicit": false, + "columns": [ + "contactInboxId" + ], + "schemaTo": "public", + "tableTo": "ContactInbox", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ContactToTagChannel_contactInboxId_ContactInbox_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ContactToTagChannel" + }, + { + "nameExplicit": false, + "columns": [ + "assignedUserId" + ], + "schemaTo": "public", + "tableTo": "User", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "Conversation_assignedUserId_User_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Conversation" + }, + { + "nameExplicit": false, + "columns": [ + "assignedInboxTeamId" + ], + "schemaTo": "public", + "tableTo": "InboxTeam", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "Conversation_assignedInboxTeamId_InboxTeam_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Conversation" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Conversation_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Conversation" + }, + { + "nameExplicit": false, + "columns": [ + "contactId" + ], + "schemaTo": "public", + "tableTo": "Contact", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Conversation_contactId_Contact_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Conversation" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ConversationParticipant_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ConversationParticipant" + }, + { + "nameExplicit": false, + "columns": [ + "conversationId" + ], + "schemaTo": "public", + "tableTo": "Conversation", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ConversationParticipant_conversationId_Conversation_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ConversationParticipant" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "User", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ConversationParticipant_userId_User_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ConversationParticipant" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Coupon_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Coupon" + }, + { + "nameExplicit": false, + "columns": [ + "topicId" + ], + "schemaTo": "public", + "tableTo": "CouponTopic", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Coupon_topicId_CouponTopic_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Coupon" + }, + { + "nameExplicit": false, + "columns": [ + "issuedContactId" + ], + "schemaTo": "public", + "tableTo": "Contact", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "Coupon_issuedContactId_Contact_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Coupon" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "CouponTopic_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "CouponTopic" + }, + { + "nameExplicit": false, + "columns": [ + "createdById" + ], + "schemaTo": "public", + "tableTo": "User", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "CouponTopic_createdById_User_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "CouponTopic" + }, + { + "nameExplicit": false, + "columns": [ + "folderId" + ], + "schemaTo": "public", + "tableTo": "Folder", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "CustomField_folderId_Folder_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "CustomField" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "CustomField_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "CustomField" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "DynamicImage_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "DynamicImage" + }, + { + "nameExplicit": false, + "columns": [ + "customFieldId" + ], + "schemaTo": "public", + "tableTo": "CustomField", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "DynamicImage_customFieldId_CustomField_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "DynamicImage" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "EmailTopic_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "EmailTopic" + }, + { + "nameExplicit": false, + "columns": [ + "folderId" + ], + "schemaTo": "public", + "tableTo": "Folder", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "EmailTopic_folderId_Folder_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "EmailTopic" + }, + { + "nameExplicit": true, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "AuditLog_workspaceId_fkey", + "entityType": "fks", + "schema": "public", + "table": "AuditLog" + }, + { + "nameExplicit": true, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "User", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "AuditLog_userId_fkey", + "entityType": "fks", + "schema": "public", + "table": "AuditLog" + }, + { + "nameExplicit": false, + "columns": [ + "tenantId" + ], + "schemaTo": "public", + "tableTo": "Tenant", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "CustomDomain_tenantId_Tenant_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "CustomDomain" + }, + { + "nameExplicit": false, + "columns": [ + "ownerId" + ], + "schemaTo": "public", + "tableTo": "User", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "Tenant_ownerId_User_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Tenant" + }, + { + "nameExplicit": false, + "columns": [ + "tenantId" + ], + "schemaTo": "public", + "tableTo": "Tenant", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "TenantHelpItem_tenantId_Tenant_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "TenantHelpItem" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "User", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "UserQuota_userId_User_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "UserQuota" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "WorkspaceUsage_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "WorkspaceUsage" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ErrorLog_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ErrorLog" + }, + { + "nameExplicit": false, + "columns": [ + "contactId" + ], + "schemaTo": "public", + "tableTo": "Contact", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "ErrorLog_contactId_Contact_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ErrorLog" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ExternalWebhook_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ExternalWebhook" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "FacebookLeadAdsAutomation_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "FacebookLeadAdsAutomation" + }, + { + "nameExplicit": false, + "columns": [ + "flowId" + ], + "schemaTo": "public", + "tableTo": "Flow", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "FacebookLeadAdsAutomation_flowId_Flow_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "FacebookLeadAdsAutomation" + }, + { + "nameExplicit": false, + "columns": [ + "automationId" + ], + "schemaTo": "public", + "tableTo": "FacebookLeadAdsAutomation", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "FacebookLeadAdsLead_n8swD949nr3L_fkey", + "entityType": "fks", + "schema": "public", + "table": "FacebookLeadAdsLead" + }, + { + "nameExplicit": false, + "columns": [ + "contactId" + ], + "schemaTo": "public", + "tableTo": "Contact", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "FacebookLeadAdsLead_contactId_Contact_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "FacebookLeadAdsLead" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "FBCommentAutomation_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "nameExplicit": false, + "columns": [ + "folderId" + ], + "schemaTo": "public", + "tableTo": "Folder", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "FBCommentAutomation_folderId_Folder_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "FBCommentAutomation" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "FBCommentAutomationEvent_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "nameExplicit": false, + "columns": [ + "automationId" + ], + "schemaTo": "public", + "tableTo": "FBCommentAutomation", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "FBCommentAutomationEvent_LOfdyJGW0vJy_fkey", + "entityType": "fks", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "nameExplicit": false, + "columns": [ + "contactId" + ], + "schemaTo": "public", + "tableTo": "Contact", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "FBCommentAutomationEvent_contactId_Contact_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "nameExplicit": false, + "columns": [ + "contactInboxId" + ], + "schemaTo": "public", + "tableTo": "ContactInbox", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "FBCommentAutomationEvent_contactInboxId_ContactInbox_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "FBCommentAutomationEvent" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "FBCommentAutomationMiss_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "FBCommentAutomationMiss" + }, + { + "nameExplicit": false, + "columns": [ + "automationId" + ], + "schemaTo": "public", + "tableTo": "FBCommentAutomation", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "FBCommentAutomationMiss_4mfgnR9RsQNo_fkey", + "entityType": "fks", + "schema": "public", + "table": "FBCommentAutomationMiss" + }, + { + "nameExplicit": false, + "columns": [ + "contactId" + ], + "schemaTo": "public", + "tableTo": "Contact", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "FBCommentAutomationMiss_contactId_Contact_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "FBCommentAutomationMiss" + }, + { + "nameExplicit": false, + "columns": [ + "contactInboxId" + ], + "schemaTo": "public", + "tableTo": "ContactInbox", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "FBCommentAutomationMiss_contactInboxId_ContactInbox_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "FBCommentAutomationMiss" + }, + { + "nameExplicit": false, + "columns": [ + "automationId" + ], + "schemaTo": "public", + "tableTo": "FBCommentAutomation", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "FBCommentAutomationReply_Q6yXIfuQcsD0_fkey", + "entityType": "fks", + "schema": "public", + "table": "FBCommentAutomationReply" + }, + { + "nameExplicit": false, + "columns": [ + "contactId" + ], + "schemaTo": "public", + "tableTo": "Contact", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "FBCommentAutomationReply_contactId_Contact_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "FBCommentAutomationReply" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "FBCommentAutomationReply_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "FBCommentAutomationReply" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "File_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "File" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "User", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "File_userId_User_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "File" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Flow_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Flow" + }, + { + "nameExplicit": false, + "columns": [ + "folderId" + ], + "schemaTo": "public", + "tableTo": "Folder", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "Flow_folderId_Folder_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Flow" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "FlowNodeStat_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "FlowNodeStat" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "FlowRun_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "FlowRun" + }, + { + "nameExplicit": false, + "columns": [ + "flowId" + ], + "schemaTo": "public", + "tableTo": "Flow", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "FlowRun_flowId_Flow_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "FlowRun" + }, + { + "nameExplicit": false, + "columns": [ + "flowVersionId" + ], + "schemaTo": "public", + "tableTo": "FlowVersion", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "FlowRun_flowVersionId_FlowVersion_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "FlowRun" + }, + { + "nameExplicit": false, + "columns": [ + "conversationId" + ], + "schemaTo": "public", + "tableTo": "Conversation", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "FlowRun_conversationId_Conversation_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "FlowRun" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "FlowVersion_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "FlowVersion" + }, + { + "nameExplicit": false, + "columns": [ + "flowId" + ], + "schemaTo": "public", + "tableTo": "Flow", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "FlowVersion_flowId_Flow_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "FlowVersion" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Folder_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Folder" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IgStoryAutomation_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IgStoryAutomation" + }, + { + "nameExplicit": false, + "columns": [ + "folderId" + ], + "schemaTo": "public", + "tableTo": "Folder", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "IgStoryAutomation_folderId_Folder_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IgStoryAutomation" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Import_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Import" + }, + { + "nameExplicit": false, + "columns": [ + "inboxId" + ], + "schemaTo": "public", + "tableTo": "Inbox", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Import_inboxId_Inbox_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Import" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "User", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "Import_userId_User_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Import" + }, + { + "nameExplicit": false, + "columns": [ + "fileId" + ], + "schemaTo": "public", + "tableTo": "File", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "Import_fileId_File_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Import" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Inbox_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Inbox" + }, + { + "nameExplicit": false, + "columns": [ + "inboxId" + ], + "schemaTo": "public", + "tableTo": "Inbox", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "InboxContactStat_inboxId_Inbox_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "InboxContactStat" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "InboxTeam_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "InboxTeam" + }, + { + "nameExplicit": false, + "columns": [ + "inboxTeamId" + ], + "schemaTo": "public", + "tableTo": "InboxTeam", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "InboxTeamMember_inboxTeamId_InboxTeam_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "InboxTeamMember" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "User", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "InboxTeamMember_userId_User_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "InboxTeamMember" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationActiveCampaign_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationActiveCampaign" + }, + { + "nameExplicit": false, + "columns": [ + "integrationId" + ], + "schemaTo": "public", + "tableTo": "Integration", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationActiveCampaign_integrationId_Integration_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationActiveCampaign" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationApi_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationApi" + }, + { + "nameExplicit": false, + "columns": [ + "inboxId" + ], + "schemaTo": "public", + "tableTo": "Inbox", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationApi_inboxId_Inbox_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationApi" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Integration_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Integration" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationClaude_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationClaude" + }, + { + "nameExplicit": false, + "columns": [ + "integrationId" + ], + "schemaTo": "public", + "tableTo": "Integration", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationClaude_integrationId_Integration_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationClaude" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationDeepseek_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationDeepseek" + }, + { + "nameExplicit": false, + "columns": [ + "integrationId" + ], + "schemaTo": "public", + "tableTo": "Integration", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationDeepseek_integrationId_Integration_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationDeepseek" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationDrip_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationDrip" + }, + { + "nameExplicit": false, + "columns": [ + "integrationId" + ], + "schemaTo": "public", + "tableTo": "Integration", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationDrip_integrationId_Integration_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationDrip" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationFacebookAds_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationFacebookAds" + }, + { + "nameExplicit": false, + "columns": [ + "integrationId" + ], + "schemaTo": "public", + "tableTo": "Integration", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationFacebookAds_integrationId_Integration_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationFacebookAds" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationGemini_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationGemini" + }, + { + "nameExplicit": false, + "columns": [ + "integrationId" + ], + "schemaTo": "public", + "tableTo": "Integration", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationGemini_integrationId_Integration_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationGemini" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationGetResponse_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationGetResponse" + }, + { + "nameExplicit": false, + "columns": [ + "integrationId" + ], + "schemaTo": "public", + "tableTo": "Integration", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationGetResponse_integrationId_Integration_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationGetResponse" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationGoogleCalendar_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationGoogleCalendar" + }, + { + "nameExplicit": false, + "columns": [ + "integrationId" + ], + "schemaTo": "public", + "tableTo": "Integration", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationGoogleCalendar_integrationId_Integration_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationGoogleCalendar" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationGoogleSheet_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationGoogleSheet" + }, + { + "nameExplicit": false, + "columns": [ + "integrationId" + ], + "schemaTo": "public", + "tableTo": "Integration", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationGoogleSheet_integrationId_Integration_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationGoogleSheet" + }, + { + "nameExplicit": true, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationInstagram_workspaceId_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "nameExplicit": true, + "columns": [ + "inboxId" + ], + "schemaTo": "public", + "tableTo": "Inbox", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationInstagram_inboxId_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "nameExplicit": true, + "columns": [ + "welcomeFlowId" + ], + "schemaTo": "public", + "tableTo": "Flow", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "IntegrationInstagram_welcomeFlowId_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationInstagram" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationKlaviyo_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationKlaviyo" + }, + { + "nameExplicit": false, + "columns": [ + "integrationId" + ], + "schemaTo": "public", + "tableTo": "Integration", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationKlaviyo_integrationId_Integration_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationKlaviyo" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationMailchimp_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationMailchimp" + }, + { + "nameExplicit": false, + "columns": [ + "integrationId" + ], + "schemaTo": "public", + "tableTo": "Integration", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationMailchimp_integrationId_Integration_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationMailchimp" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationMailerLite_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationMailerLite" + }, + { + "nameExplicit": false, + "columns": [ + "integrationId" + ], + "schemaTo": "public", + "tableTo": "Integration", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationMailerLite_integrationId_Integration_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationMailerLite" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationMessenger_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "nameExplicit": false, + "columns": [ + "inboxId" + ], + "schemaTo": "public", + "tableTo": "Inbox", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationMessenger_inboxId_Inbox_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "nameExplicit": false, + "columns": [ + "welcomeFlowId" + ], + "schemaTo": "public", + "tableTo": "Flow", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "IntegrationMessenger_welcomeFlowId_Flow_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationMessenger" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationMetaCatalog_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "nameExplicit": false, + "columns": [ + "integrationId" + ], + "schemaTo": "public", + "tableTo": "Integration", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationMetaCatalog_integrationId_Integration_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationMetaCatalog" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationMoosend_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationMoosend" + }, + { + "nameExplicit": false, + "columns": [ + "integrationId" + ], + "schemaTo": "public", + "tableTo": "Integration", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationMoosend_integrationId_Integration_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationMoosend" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationOpenai_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationOpenai" + }, + { + "nameExplicit": false, + "columns": [ + "integrationId" + ], + "schemaTo": "public", + "tableTo": "Integration", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationOpenai_integrationId_Integration_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationOpenai" + }, + { + "nameExplicit": false, + "columns": [ + "aiAgentId" + ], + "schemaTo": "public", + "tableTo": "AIAgent", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "IntegrationOpenai_aiAgentId_AIAgent_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationOpenai" + }, + { + "nameExplicit": false, + "columns": [ + "integrationId" + ], + "schemaTo": "public", + "tableTo": "Integration", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationOpenaiCompatible_integrationId_Integration_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationOpenaiCompatible" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationOpenaiCompatible_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationOpenaiCompatible" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationOpenrouter_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationOpenrouter" + }, + { + "nameExplicit": false, + "columns": [ + "integrationId" + ], + "schemaTo": "public", + "tableTo": "Integration", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationOpenrouter_integrationId_Integration_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationOpenrouter" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationOutlookCalendar_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationOutlookCalendar" + }, + { + "nameExplicit": false, + "columns": [ + "integrationId" + ], + "schemaTo": "public", + "tableTo": "Integration", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationOutlookCalendar_integrationId_Integration_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationOutlookCalendar" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationSendGrid_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationSendGrid" + }, + { + "nameExplicit": false, + "columns": [ + "integrationId" + ], + "schemaTo": "public", + "tableTo": "Integration", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationSendGrid_integrationId_Integration_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationSendGrid" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationSmtp_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationSmtp" + }, + { + "nameExplicit": false, + "columns": [ + "inboxId" + ], + "schemaTo": "public", + "tableTo": "Inbox", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationSmtp_inboxId_Inbox_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationSmtp" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationTelegram_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationTelegram" + }, + { + "nameExplicit": false, + "columns": [ + "inboxId" + ], + "schemaTo": "public", + "tableTo": "Inbox", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationTelegram_inboxId_Inbox_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationTelegram" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationTiktok_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationTiktok" + }, + { + "nameExplicit": false, + "columns": [ + "inboxId" + ], + "schemaTo": "public", + "tableTo": "Inbox", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationTiktok_inboxId_Inbox_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationTiktok" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationWebchat_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationWebchat" + }, + { + "nameExplicit": false, + "columns": [ + "inboxId" + ], + "schemaTo": "public", + "tableTo": "Inbox", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationWebchat_inboxId_Inbox_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationWebchat" + }, + { + "nameExplicit": false, + "columns": [ + "welcomeFlowId" + ], + "schemaTo": "public", + "tableTo": "Flow", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "IntegrationWebchat_welcomeFlowId_Flow_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationWebchat" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationWhatsapp_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "nameExplicit": false, + "columns": [ + "inboxId" + ], + "schemaTo": "public", + "tableTo": "Inbox", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationWhatsapp_inboxId_Inbox_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationZalo_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationZalo" + }, + { + "nameExplicit": false, + "columns": [ + "inboxId" + ], + "schemaTo": "public", + "tableTo": "Inbox", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "IntegrationZalo_inboxId_Inbox_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationZalo" + }, + { + "nameExplicit": false, + "columns": [ + "fallbackFlowId" + ], + "schemaTo": "public", + "tableTo": "Flow", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "IntegrationZalo_fallbackFlowId_Flow_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "IntegrationZalo" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "MagicLink_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "MagicLink" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "MagicLinkStat_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "MagicLinkStat" + }, + { + "nameExplicit": false, + "columns": [ + "linkId" + ], + "schemaTo": "public", + "tableTo": "MagicLink", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "MagicLinkStat_linkId_MagicLink_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "MagicLinkStat" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "MediaLibraryFile_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "MediaLibraryFile" + }, + { + "nameExplicit": false, + "columns": [ + "folderId" + ], + "schemaTo": "public", + "tableTo": "MediaLibraryFolder", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "MediaLibraryFile_folderId_MediaLibraryFolder_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "MediaLibraryFile" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "MediaLibraryFolder_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "MediaLibraryFolder" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "MessagingAdOperation_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "nameExplicit": false, + "columns": [ + "integrationWhatsappId" + ], + "schemaTo": "public", + "tableTo": "IntegrationWhatsapp", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "MessagingAdOperation_D2X0VlACjh0B_fkey", + "entityType": "fks", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "nameExplicit": false, + "columns": [ + "integrationMessengerId" + ], + "schemaTo": "public", + "tableTo": "IntegrationMessenger", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "MessagingAdOperation_DlCmuyYqclNt_fkey", + "entityType": "fks", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "nameExplicit": false, + "columns": [ + "integrationInstagramId" + ], + "schemaTo": "public", + "tableTo": "IntegrationInstagram", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "MessagingAdOperation_LwL2ykeZegup_fkey", + "entityType": "fks", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "nameExplicit": false, + "columns": [ + "createdBy" + ], + "schemaTo": "public", + "tableTo": "User", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "MessagingAdOperation_createdBy_User_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "MessagingAdsConnection_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "MessagingAdsConnection" + }, + { + "nameExplicit": false, + "columns": [ + "integrationWhatsappId" + ], + "schemaTo": "public", + "tableTo": "IntegrationWhatsapp", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "MessagingAdsConnection_Q2FrBLiP3QqD_fkey", + "entityType": "fks", + "schema": "public", + "table": "MessagingAdsConnection" + }, + { + "nameExplicit": false, + "columns": [ + "integrationMessengerId" + ], + "schemaTo": "public", + "tableTo": "IntegrationMessenger", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "MessagingAdsConnection_tLZC78O5pMBb_fkey", + "entityType": "fks", + "schema": "public", + "table": "MessagingAdsConnection" + }, + { + "nameExplicit": false, + "columns": [ + "integrationInstagramId" + ], + "schemaTo": "public", + "tableTo": "IntegrationInstagram", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "MessagingAdsConnection_4ubezEjd9jQX_fkey", + "entityType": "fks", + "schema": "public", + "table": "MessagingAdsConnection" + }, + { + "nameExplicit": false, + "columns": [ + "integrationMessengerId" + ], + "schemaTo": "public", + "tableTo": "IntegrationMessenger", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "MessengerMessageTemplate_x0Vv1d8cvLYN_fkey", + "entityType": "fks", + "schema": "public", + "table": "MessengerMessageTemplate" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "MetaCapiEvent_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "nameExplicit": false, + "columns": [ + "contactInboxId" + ], + "schemaTo": "public", + "tableTo": "ContactInbox", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "MetaCapiEvent_contactInboxId_ContactInbox_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "nameExplicit": false, + "columns": [ + "integrationMetaCatalogId" + ], + "schemaTo": "public", + "tableTo": "IntegrationMetaCatalog", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "MetaCatalogItem_wJXyKUssR08y_fkey", + "entityType": "fks", + "schema": "public", + "table": "MetaCatalogItem" + }, + { + "nameExplicit": false, + "columns": [ + "productId" + ], + "schemaTo": "public", + "tableTo": "Product", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "MetaCatalogItem_productId_Product_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "MetaCatalogItem" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "MetaCatalogSyncRun_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "nameExplicit": false, + "columns": [ + "integrationMetaCatalogId" + ], + "schemaTo": "public", + "tableTo": "IntegrationMetaCatalog", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "MetaCatalogSyncRun_8PnmCJ0uISUd_fkey", + "entityType": "fks", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "nameExplicit": false, + "columns": [ + "categoryId" + ], + "schemaTo": "public", + "tableTo": "ProductCategory", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "MetaCatalogSyncRun_categoryId_ProductCategory_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "MetaCatalogSyncRun" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Minigame_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Minigame" + }, + { + "nameExplicit": false, + "columns": [ + "minigameId" + ], + "schemaTo": "public", + "tableTo": "Minigame", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "MinigameContact_minigameId_Minigame_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "MinigameContact" + }, + { + "nameExplicit": false, + "columns": [ + "contactId" + ], + "schemaTo": "public", + "tableTo": "Contact", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "MinigameContact_contactId_Contact_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "MinigameContact" + }, + { + "nameExplicit": false, + "columns": [ + "referrerContactId" + ], + "schemaTo": "public", + "tableTo": "Contact", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "MinigameContact_referrerContactId_Contact_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "MinigameContact" + }, + { + "nameExplicit": false, + "columns": [ + "contactInboxId" + ], + "schemaTo": "public", + "tableTo": "ContactInbox", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "MinigameContact_contactInboxId_ContactInbox_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "MinigameContact" + }, + { + "nameExplicit": false, + "columns": [ + "minigameId" + ], + "schemaTo": "public", + "tableTo": "Minigame", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "MinigamePlay_minigameId_Minigame_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "MinigamePlay" + }, + { + "nameExplicit": false, + "columns": [ + "contactId" + ], + "schemaTo": "public", + "tableTo": "Contact", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "MinigamePlay_contactId_Contact_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "MinigamePlay" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "User", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Credential_userId_User_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "PlatformCredential" + }, + { + "nameExplicit": false, + "columns": [ + "categoryId" + ], + "schemaTo": "public", + "tableTo": "ProductCategory", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "Product_categoryId_ProductCategory_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Product" + }, + { + "nameExplicit": false, + "columns": [ + "subcategoryId" + ], + "schemaTo": "public", + "tableTo": "ProductCategory", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "Product_subcategoryId_ProductCategory_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Product" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Product_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Product" + }, + { + "nameExplicit": false, + "columns": [ + "productId" + ], + "schemaTo": "public", + "tableTo": "Product", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ProductAddon_productId_Product_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ProductAddon" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ProductCategory_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ProductCategory" + }, + { + "nameExplicit": false, + "columns": [ + "parentId" + ], + "schemaTo": "public", + "tableTo": "ProductCategory", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ProductCategory_parentId_ProductCategory_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ProductCategory" + }, + { + "nameExplicit": false, + "columns": [ + "productId" + ], + "schemaTo": "public", + "tableTo": "Product", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ProductVariant_productId_Product_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ProductVariant" + }, + { + "nameExplicit": false, + "columns": [ + "productId" + ], + "schemaTo": "public", + "tableTo": "Product", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "ProductVariantOption_productId_Product_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "ProductVariantOption" + }, + { + "nameExplicit": false, + "columns": [ + "submissionId" + ], + "schemaTo": "public", + "tableTo": "QuestionnaireSubmission", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "QuestionnaireAnswer_l8clOEM7NsPl_fkey", + "entityType": "fks", + "schema": "public", + "table": "QuestionnaireAnswer" + }, + { + "nameExplicit": false, + "columns": [ + "questionId" + ], + "schemaTo": "public", + "tableTo": "QuestionnaireQuestion", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "QuestionnaireAnswer_questionId_QuestionnaireQuestion_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "QuestionnaireAnswer" + }, + { + "nameExplicit": false, + "columns": [ + "triggerFlowId" + ], + "schemaTo": "public", + "tableTo": "Flow", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "Questionnaire_triggerFlowId_Flow_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Questionnaire" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Questionnaire_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Questionnaire" + }, + { + "nameExplicit": false, + "columns": [ + "questionnaireId" + ], + "schemaTo": "public", + "tableTo": "Questionnaire", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "QuestionnaireQuestion_questionnaireId_Questionnaire_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "QuestionnaireQuestion" + }, + { + "nameExplicit": false, + "columns": [ + "customFieldId" + ], + "schemaTo": "public", + "tableTo": "CustomField", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "QuestionnaireQuestion_customFieldId_CustomField_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "QuestionnaireQuestion" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "QuestionnaireSubmission_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "QuestionnaireSubmission" + }, + { + "nameExplicit": false, + "columns": [ + "questionnaireId" + ], + "schemaTo": "public", + "tableTo": "Questionnaire", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "QuestionnaireSubmission_questionnaireId_Questionnaire_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "QuestionnaireSubmission" + }, + { + "nameExplicit": false, + "columns": [ + "contactId" + ], + "schemaTo": "public", + "tableTo": "Contact", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "QuestionnaireSubmission_contactId_Contact_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "QuestionnaireSubmission" + }, + { + "nameExplicit": false, + "columns": [ + "conversationId" + ], + "schemaTo": "public", + "tableTo": "Conversation", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "QuestionnaireSubmission_conversationId_Conversation_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "QuestionnaireSubmission" + }, + { + "nameExplicit": false, + "columns": [ + "currentQuestionId" + ], + "schemaTo": "public", + "tableTo": "QuestionnaireQuestion", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "QuestionnaireSubmission_mha0bYFxcV7A_fkey", + "entityType": "fks", + "schema": "public", + "table": "QuestionnaireSubmission" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "RefLinkStat_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "RefLinkStat" + }, + { + "nameExplicit": false, + "columns": [ + "linkId" + ], + "schemaTo": "public", + "tableTo": "Reflink", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "RefLinkStat_linkId_Reflink_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "RefLinkStat" + }, + { + "nameExplicit": false, + "columns": [ + "flowId" + ], + "schemaTo": "public", + "tableTo": "Flow", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Reflink_flowId_Flow_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Reflink" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Reflink_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Reflink" + }, + { + "nameExplicit": false, + "columns": [ + "customFieldId" + ], + "schemaTo": "public", + "tableTo": "CustomField", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "Reflink_customFieldId_CustomField_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Reflink" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "SavedReply_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "SavedReply" + }, + { + "nameExplicit": false, + "columns": [ + "folderId" + ], + "schemaTo": "public", + "tableTo": "Folder", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "Sequence_folderId_Folder_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Sequence" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Sequence_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Sequence" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "SequenceDispatch_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "nameExplicit": false, + "columns": [ + "sequenceId" + ], + "schemaTo": "public", + "tableTo": "Sequence", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "SequenceDispatch_sequenceId_Sequence_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "nameExplicit": false, + "columns": [ + "contactId" + ], + "schemaTo": "public", + "tableTo": "Contact", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "SequenceDispatch_contactId_Contact_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "nameExplicit": false, + "columns": [ + "contactInboxId" + ], + "schemaTo": "public", + "tableTo": "ContactInbox", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "SequenceDispatch_contactInboxId_ContactInbox_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "nameExplicit": false, + "columns": [ + "stepId" + ], + "schemaTo": "public", + "tableTo": "SequenceStep", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "SequenceDispatch_stepId_SequenceStep_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "nameExplicit": true, + "columns": [ + "enrollmentId", + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "ContactOnSequence", + "columnsTo": [ + "id", + "workspaceId" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "SequenceDispatch_enrollment_workspace_fkey", + "entityType": "fks", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "nameExplicit": false, + "columns": [ + "flowId" + ], + "schemaTo": "public", + "tableTo": "Flow", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "SequenceStep_flowId_Flow_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "SequenceStep" + }, + { + "nameExplicit": false, + "columns": [ + "sequenceId" + ], + "schemaTo": "public", + "tableTo": "Sequence", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "SequenceStep_sequenceId_Sequence_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "SequenceStep" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Spreadsheet_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Spreadsheet" + }, + { + "nameExplicit": false, + "columns": [ + "folderId" + ], + "schemaTo": "public", + "tableTo": "Folder", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "Tag_folderId_Folder_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Tag" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Tag_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Tag" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "TagChannel_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "TagChannel" + }, + { + "nameExplicit": false, + "columns": [ + "tagId" + ], + "schemaTo": "public", + "tableTo": "Tag", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "TagChannel_tagId_Tag_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "TagChannel" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Template_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Template" + }, + { + "nameExplicit": false, + "columns": [ + "tenantId" + ], + "schemaTo": "public", + "tableTo": "Tenant", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "Template_tenantId_Tenant_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Template" + }, + { + "nameExplicit": false, + "columns": [ + "createdBy" + ], + "schemaTo": "public", + "tableTo": "User", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "Template_createdBy_User_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Template" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "TemplateInstallation_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "nameExplicit": false, + "columns": [ + "templateId" + ], + "schemaTo": "public", + "tableTo": "Template", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "TemplateInstallation_templateId_Template_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "nameExplicit": false, + "columns": [ + "installFolderId" + ], + "schemaTo": "public", + "tableTo": "Folder", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "TemplateInstallation_installFolderId_Folder_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "nameExplicit": false, + "columns": [ + "installedBy" + ], + "schemaTo": "public", + "tableTo": "User", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "TemplateInstallation_installedBy_User_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "TemplateInstallation" + }, + { + "nameExplicit": false, + "columns": [ + "installationId" + ], + "schemaTo": "public", + "tableTo": "TemplateInstallation", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "TemplateInstalledResource_a0BqwWeO96EB_fkey", + "entityType": "fks", + "schema": "public", + "table": "TemplateInstalledResource" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "TemplateInstalledResource_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "TemplateInstalledResource" + }, + { + "nameExplicit": false, + "columns": [ + "folderId" + ], + "schemaTo": "public", + "tableTo": "Folder", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "Trigger_folderId_Folder_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Trigger" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Trigger_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Trigger" + }, + { + "nameExplicit": false, + "columns": [ + "triggerId" + ], + "schemaTo": "public", + "tableTo": "Trigger", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Condition_triggerId_Trigger_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Condition" + }, + { + "nameExplicit": false, + "columns": [ + "webhookId" + ], + "schemaTo": "public", + "tableTo": "Webhook", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Condition_webhookId_Webhook_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Condition" + }, + { + "nameExplicit": false, + "columns": [ + "triggerId" + ], + "schemaTo": "public", + "tableTo": "Trigger", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "TriggerContactHistory_triggerId_Trigger_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "TriggerContactHistory" + }, + { + "nameExplicit": false, + "columns": [ + "contactId" + ], + "schemaTo": "public", + "tableTo": "Contact", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "TriggerContactHistory_contactId_Contact_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "TriggerContactHistory" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "TriggerContactHistory_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "TriggerContactHistory" + }, + { + "nameExplicit": false, + "columns": [ + "triggerId" + ], + "schemaTo": "public", + "tableTo": "Trigger", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "TriggerExecution_triggerId_Trigger_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "TriggerExecution" + }, + { + "nameExplicit": false, + "columns": [ + "contactId" + ], + "schemaTo": "public", + "tableTo": "Contact", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "TriggerExecution_contactId_Contact_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "TriggerExecution" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "TriggerExecution_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "TriggerExecution" + }, + { + "nameExplicit": false, + "columns": [ + "triggerId" + ], + "schemaTo": "public", + "tableTo": "Trigger", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "TriggerStat_triggerId_Trigger_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "TriggerStat" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "TriggerStat_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "TriggerStat" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "User", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "UserDeviceToken_userId_User_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "UserDeviceToken" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "UserDeviceToken_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "UserDeviceToken" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "UserPersistentMenu_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "UserPersistentMenu" + }, + { + "nameExplicit": false, + "columns": [ + "folderId" + ], + "schemaTo": "public", + "tableTo": "Folder", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "Webhook_folderId_Folder_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Webhook" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "Webhook_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Webhook" + }, + { + "nameExplicit": false, + "columns": [ + "webhookId" + ], + "schemaTo": "public", + "tableTo": "Webhook", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "WebhookExecution_webhookId_Webhook_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "WebhookExecution" + }, + { + "nameExplicit": false, + "columns": [ + "contactId" + ], + "schemaTo": "public", + "tableTo": "Contact", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "WebhookExecution_contactId_Contact_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "WebhookExecution" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "WebhookExecution_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "WebhookExecution" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "WhatsappBusinessAccount_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "WhatsappBusinessAccount" + }, + { + "nameExplicit": false, + "columns": [ + "integrationWhatsappId" + ], + "schemaTo": "public", + "tableTo": "IntegrationWhatsapp", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "WhatsappFlow_integrationWhatsappId_IntegrationWhatsapp_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "WhatsappFlow" + }, + { + "nameExplicit": false, + "columns": [ + "integrationWhatsappId" + ], + "schemaTo": "public", + "tableTo": "IntegrationWhatsapp", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "WhatsappMessageTemplate_p6pSomUTTJCm_fkey", + "entityType": "fks", + "schema": "public", + "table": "WhatsappMessageTemplate" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "User", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "WhatsappSignupSession_userId_User_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "WhatsappSignupSession" + }, + { + "nameExplicit": false, + "columns": [ + "ownerId" + ], + "schemaTo": "public", + "tableTo": "User", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "WhatsappSignupSession_ownerId_User_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "WhatsappSignupSession" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "WhatsappSignupSession_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "WhatsappSignupSession" + }, + { + "nameExplicit": false, + "columns": [ + "ownerId" + ], + "schemaTo": "public", + "tableTo": "User", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "Workspace_userId_User_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Workspace" + }, + { + "nameExplicit": false, + "columns": [ + "tenantId" + ], + "schemaTo": "public", + "tableTo": "Tenant", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "Workspace_tenantId_Tenant_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "Workspace" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "WorkspaceApiToken_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "WorkspaceApiToken" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "WorkspaceMac_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "WorkspaceMac" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "schemaTo": "public", + "tableTo": "Workspace", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "WorkspaceMember_workspaceId_Workspace_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "WorkspaceMember" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "User", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "WorkspaceMember_userId_User_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "WorkspaceMember" + }, + { + "columns": [ + "occurredAt", + "eventId" + ], + "nameExplicit": false, + "name": "AnalyticsBotMessageEvent_pkey", + "entityType": "pks", + "schema": "public", + "table": "AnalyticsBotMessageEvent" + }, + { + "columns": [ + "broadcastId", + "contactInboxId", + "batchId", + "eventType", + "occurredAt" + ], + "nameExplicit": false, + "name": "AnalyticsBroadcastEvent_pkey", + "entityType": "pks", + "schema": "public", + "table": "AnalyticsBroadcastEvent" + }, + { + "columns": [ + "occurredAt", + "eventId" + ], + "nameExplicit": false, + "name": "AnalyticsContactEvent_pkey", + "entityType": "pks", + "schema": "public", + "table": "AnalyticsContactEvent" + }, + { + "columns": [ + "occurredAt", + "eventId" + ], + "nameExplicit": false, + "name": "AnalyticsConversationEvent_pkey", + "entityType": "pks", + "schema": "public", + "table": "AnalyticsConversationEvent" + }, + { + "columns": [ + "flowId", + "analyticsId", + "nodeId", + "buttonId", + "contactInboxId", + "eventType", + "occurredAt" + ], + "nameExplicit": false, + "name": "AnalyticsFlowNodeEvent_pkey", + "entityType": "pks", + "schema": "public", + "table": "AnalyticsFlowNodeEvent" + }, + { + "columns": [ + "occurredAt", + "eventId" + ], + "nameExplicit": false, + "name": "AnalyticsMessageEvent_pkey", + "entityType": "pks", + "schema": "public", + "table": "AnalyticsMessageEvent" + }, + { + "columns": [ + "sequenceId", + "stepId", + "contactInboxId", + "eventType", + "occurredAt" + ], + "nameExplicit": false, + "name": "AnalyticsSequenceEvent_pkey", + "entityType": "pks", + "schema": "public", + "table": "AnalyticsSequenceEvent" + }, + { + "columns": [ + "id", + "createdAt" + ], + "nameExplicit": false, + "name": "Attachment_pkey", + "entityType": "pks", + "schema": "public", + "table": "Attachment" + }, + { + "columns": [ + "workspaceId", + "contactInboxId", + "throttleType", + "subjectId" + ], + "nameExplicit": true, + "name": "AutomationThrottle_pkey", + "entityType": "pks", + "schema": "public", + "table": "AutomationThrottle" + }, + { + "columns": [ + "broadcastId", + "inboxId" + ], + "nameExplicit": true, + "name": "BroadcastTarget_pkey", + "entityType": "pks", + "schema": "public", + "table": "BroadcastTarget" + }, + { + "columns": [ + "workspaceId", + "hourBucket", + "contactInboxId" + ], + "nameExplicit": false, + "name": "ContactActiveHourly_pkey", + "entityType": "pks", + "schema": "public", + "table": "ContactActiveHourly" + }, + { + "columns": [ + "workspaceId", + "periodStart", + "contactInboxId" + ], + "nameExplicit": false, + "name": "ContactActiveMonthly_pkey", + "entityType": "pks", + "schema": "public", + "table": "ContactActiveMonthly" + }, + { + "columns": [ + "broadcastId", + "contactId" + ], + "nameExplicit": true, + "name": "ContactsOnBroadcast_pkey", + "entityType": "pks", + "schema": "public", + "table": "ContactOnBroadcast" + }, + { + "columns": [ + "id", + "workspaceId" + ], + "nameExplicit": true, + "name": "ContactOnSequence_pkey", + "entityType": "pks", + "schema": "public", + "table": "ContactOnSequence" + }, + { + "columns": [ + "contactId", + "tagId" + ], + "nameExplicit": false, + "name": "ContactToTag_pkey", + "entityType": "pks", + "schema": "public", + "table": "ContactToTag" + }, + { + "columns": [ + "tagChannelId", + "contactInboxId" + ], + "nameExplicit": false, + "name": "ContactToTagChannel_pkey", + "entityType": "pks", + "schema": "public", + "table": "ContactToTagChannel" + }, + { + "columns": [ + "id", + "createdAt" + ], + "nameExplicit": false, + "name": "Message_pkey", + "entityType": "pks", + "schema": "public", + "table": "Message" + }, + { + "columns": [ + "id", + "workspaceId" + ], + "nameExplicit": true, + "name": "SequenceDispatch_pkey", + "entityType": "pks", + "schema": "public", + "table": "SequenceDispatch" + }, + { + "columns": [ + "id", + "contactId" + ], + "nameExplicit": true, + "name": "TriggerContactHistory_pkey", + "entityType": "pks", + "schema": "public", + "table": "TriggerContactHistory" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "MessageShard_pkey", + "schema": "public", + "table": "MessageShard", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "ShardTimeRange_pkey", + "schema": "public", + "table": "ShardTimeRange", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "AdsConversionEvent_pkey", + "schema": "public", + "table": "AdsConversionEvent", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "AdsConversionRule_pkey", + "schema": "public", + "table": "AdsConversionRule", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "AIAgent_pkey", + "schema": "public", + "table": "AIAgent", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "AIConversationEmbedding_pkey", + "schema": "public", + "table": "AIConversationEmbedding", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "AIConversationSource_pkey", + "schema": "public", + "table": "AIConversationSource", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "AIEmbedding_pkey", + "schema": "public", + "table": "AIEmbedding", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "AIFile_pkey", + "schema": "public", + "table": "AIFile", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "AIFunction_pkey", + "schema": "public", + "table": "AIFunction", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "AIMCPServer_pkey", + "schema": "public", + "table": "AIMCPServer", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "AnalyticsEmailTopic_pkey", + "schema": "public", + "table": "AnalyticsEmailTopic", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Appointment_pkey", + "schema": "public", + "table": "Appointment", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "AppointmentCalendar_pkey", + "schema": "public", + "table": "AppointmentCalendar", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "AppointmentCalendarAvailability_pkey", + "schema": "public", + "table": "AppointmentCalendarAvailability", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "AppointmentCalendarReminder_pkey", + "schema": "public", + "table": "AppointmentCalendarReminder", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "AppointmentReminderDispatch_pkey", + "schema": "public", + "table": "AppointmentReminderDispatch", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Account_pkey", + "schema": "public", + "table": "Account", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Invitation_pkey", + "schema": "public", + "table": "Invitation", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Jwk_pkey", + "schema": "public", + "table": "Jwk", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Session_pkey", + "schema": "public", + "table": "Session", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "User_pkey", + "schema": "public", + "table": "User", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Verification_pkey", + "schema": "public", + "table": "Verification", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "AutomatedResponse_pkey", + "schema": "public", + "table": "AutomatedResponse", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "BotField_pkey", + "schema": "public", + "table": "BotField", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Broadcast_pkey", + "schema": "public", + "table": "Broadcast", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "CoexistSyncRun_pkey", + "schema": "public", + "table": "CoexistSyncRun", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "ConnectSession_pkey", + "schema": "public", + "table": "ConnectSession", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Connection_pkey", + "schema": "public", + "table": "Connection", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Contact_pkey", + "schema": "public", + "table": "Contact", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "ContactCustomField_pkey", + "schema": "public", + "table": "ContactCustomField", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "ContactInbox_pkey", + "schema": "public", + "table": "ContactInbox", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "ContactNote_pkey", + "schema": "public", + "table": "ContactNote", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "ContactOnSmartDelay_pkey", + "schema": "public", + "table": "ContactOnSmartDelay", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Conversation_pkey", + "schema": "public", + "table": "Conversation", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "ConversationParticipant_pkey", + "schema": "public", + "table": "ConversationParticipant", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Coupon_pkey", + "schema": "public", + "table": "Coupon", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "CouponTopic_pkey", + "schema": "public", + "table": "CouponTopic", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "CustomField_pkey", + "schema": "public", + "table": "CustomField", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "DynamicImage_pkey", + "schema": "public", + "table": "DynamicImage", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "EmailTopic_pkey", + "schema": "public", + "table": "EmailTopic", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "AuditLog_pkey", + "schema": "public", + "table": "AuditLog", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "CustomDomain_pkey", + "schema": "public", + "table": "CustomDomain", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Tenant_pkey", + "schema": "public", + "table": "Tenant", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "TenantHelpItem_pkey", + "schema": "public", + "table": "TenantHelpItem", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "UserQuota_pkey", + "schema": "public", + "table": "UserQuota", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "WorkspaceUsage_pkey", + "schema": "public", + "table": "WorkspaceUsage", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "ErrorLog_pkey", + "schema": "public", + "table": "ErrorLog", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "ExternalWebhook_pkey", + "schema": "public", + "table": "ExternalWebhook", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "FacebookLeadAdsAutomation_pkey", + "schema": "public", + "table": "FacebookLeadAdsAutomation", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "FacebookLeadAdsLead_pkey", + "schema": "public", + "table": "FacebookLeadAdsLead", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "FBCommentAutomation_pkey", + "schema": "public", + "table": "FBCommentAutomation", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "FBCommentAutomationEvent_pkey", + "schema": "public", + "table": "FBCommentAutomationEvent", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "FBCommentAutomationMiss_pkey", + "schema": "public", + "table": "FBCommentAutomationMiss", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "FBCommentAutomationReply_pkey", + "schema": "public", + "table": "FBCommentAutomationReply", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "File_pkey", + "schema": "public", + "table": "File", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Flow_pkey", + "schema": "public", + "table": "Flow", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "FlowAnalyticsSession_pkey", + "schema": "public", + "table": "FlowAnalyticsSession", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "FlowNodeStat_pkey", + "schema": "public", + "table": "FlowNodeStat", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "FlowRun_pkey", + "schema": "public", + "table": "FlowRun", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "FlowVersion_pkey", + "schema": "public", + "table": "FlowVersion", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Folder_pkey", + "schema": "public", + "table": "Folder", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IgStoryAutomation_pkey", + "schema": "public", + "table": "IgStoryAutomation", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Import_pkey", + "schema": "public", + "table": "Import", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Inbox_pkey", + "schema": "public", + "table": "Inbox", + "entityType": "pks" + }, + { + "columns": [ + "inboxId" + ], + "nameExplicit": false, + "name": "InboxContactStat_pkey", + "schema": "public", + "table": "InboxContactStat", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "InboxTeam_pkey", + "schema": "public", + "table": "InboxTeam", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "InboxTeamMember_pkey", + "schema": "public", + "table": "InboxTeamMember", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationActiveCampaign_pkey", + "schema": "public", + "table": "IntegrationActiveCampaign", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationApi_pkey", + "schema": "public", + "table": "IntegrationApi", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Integration_pkey", + "schema": "public", + "table": "Integration", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationClaude_pkey", + "schema": "public", + "table": "IntegrationClaude", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationDeepseek_pkey", + "schema": "public", + "table": "IntegrationDeepseek", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationDrip_pkey", + "schema": "public", + "table": "IntegrationDrip", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationFacebookAds_pkey", + "schema": "public", + "table": "IntegrationFacebookAds", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationGemini_pkey", + "schema": "public", + "table": "IntegrationGemini", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationGetResponse_pkey", + "schema": "public", + "table": "IntegrationGetResponse", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationGoogleCalendar_pkey", + "schema": "public", + "table": "IntegrationGoogleCalendar", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationGoogleSheet_pkey", + "schema": "public", + "table": "IntegrationGoogleSheet", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationInstagram_pkey", + "schema": "public", + "table": "IntegrationInstagram", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationKlaviyo_pkey", + "schema": "public", + "table": "IntegrationKlaviyo", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationMailchimp_pkey", + "schema": "public", + "table": "IntegrationMailchimp", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationMailerLite_pkey", + "schema": "public", + "table": "IntegrationMailerLite", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationMessenger_pkey", + "schema": "public", + "table": "IntegrationMessenger", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationMetaCatalog_pkey", + "schema": "public", + "table": "IntegrationMetaCatalog", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationMoosend_pkey", + "schema": "public", + "table": "IntegrationMoosend", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationOpenai_pkey", + "schema": "public", + "table": "IntegrationOpenai", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationOpenaiCompatible_pkey", + "schema": "public", + "table": "IntegrationOpenaiCompatible", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationOpenrouter_pkey", + "schema": "public", + "table": "IntegrationOpenrouter", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationOutlookCalendar_pkey", + "schema": "public", + "table": "IntegrationOutlookCalendar", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationSendGrid_pkey", + "schema": "public", + "table": "IntegrationSendGrid", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationSmtp_pkey", + "schema": "public", + "table": "IntegrationSmtp", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationTelegram_pkey", + "schema": "public", + "table": "IntegrationTelegram", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationTiktok_pkey", + "schema": "public", + "table": "IntegrationTiktok", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationWebchat_pkey", + "schema": "public", + "table": "IntegrationWebchat", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationWhatsapp_pkey", + "schema": "public", + "table": "IntegrationWhatsapp", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "IntegrationZalo_pkey", + "schema": "public", + "table": "IntegrationZalo", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "MagicLink_pkey", + "schema": "public", + "table": "MagicLink", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "MediaLibraryFile_pkey", + "schema": "public", + "table": "MediaLibraryFile", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "MediaLibraryFolder_pkey", + "schema": "public", + "table": "MediaLibraryFolder", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "MessageCleanup_pkey", + "schema": "public", + "table": "MessageCleanup", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "MessagingAdOperation_pkey", + "schema": "public", + "table": "MessagingAdOperation", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "MessagingAdsConnection_pkey", + "schema": "public", + "table": "MessagingAdsConnection", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "MessengerMessageTemplate_pkey", + "schema": "public", + "table": "MessengerMessageTemplate", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "MetaCapiEvent_pkey", + "schema": "public", + "table": "MetaCapiEvent", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "MetaCatalogItem_pkey", + "schema": "public", + "table": "MetaCatalogItem", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "MetaCatalogSyncRun_pkey", + "schema": "public", + "table": "MetaCatalogSyncRun", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Minigame_pkey", + "schema": "public", + "table": "Minigame", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "MinigameContact_pkey", + "schema": "public", + "table": "MinigameContact", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "MinigamePlay_pkey", + "schema": "public", + "table": "MinigamePlay", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Credential_pkey", + "schema": "public", + "table": "PlatformCredential", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Product_pkey", + "schema": "public", + "table": "Product", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "ProductAddon_pkey", + "schema": "public", + "table": "ProductAddon", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "ProductCategory_pkey", + "schema": "public", + "table": "ProductCategory", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "ProductVariant_pkey", + "schema": "public", + "table": "ProductVariant", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "ProductVariantOption_pkey", + "schema": "public", + "table": "ProductVariantOption", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "QuestionnaireAnswer_pkey", + "schema": "public", + "table": "QuestionnaireAnswer", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Questionnaire_pkey", + "schema": "public", + "table": "Questionnaire", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "QuestionnaireQuestion_pkey", + "schema": "public", + "table": "QuestionnaireQuestion", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "QuestionnaireSubmission_pkey", + "schema": "public", + "table": "QuestionnaireSubmission", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Reflink_pkey", + "schema": "public", + "table": "Reflink", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "SavedReply_pkey", + "schema": "public", + "table": "SavedReply", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Sequence_pkey", + "schema": "public", + "table": "Sequence", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "SequenceStep_pkey", + "schema": "public", + "table": "SequenceStep", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Spreadsheet_pkey", + "schema": "public", + "table": "Spreadsheet", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "SystemField_pkey", + "schema": "public", + "table": "SystemField", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Tag_pkey", + "schema": "public", + "table": "Tag", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "TagChannel_pkey", + "schema": "public", + "table": "TagChannel", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Template_pkey", + "schema": "public", + "table": "Template", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "TemplateInstallation_pkey", + "schema": "public", + "table": "TemplateInstallation", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "TemplateInstalledResource_pkey", + "schema": "public", + "table": "TemplateInstalledResource", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Trigger_pkey", + "schema": "public", + "table": "Trigger", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Condition_pkey", + "schema": "public", + "table": "Condition", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "TriggerExecution_pkey", + "schema": "public", + "table": "TriggerExecution", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "TriggerStat_pkey", + "schema": "public", + "table": "TriggerStat", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "UserDeviceToken_pkey", + "schema": "public", + "table": "UserDeviceToken", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "UserPersistentMenu_pkey", + "schema": "public", + "table": "UserPersistentMenu", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Webhook_pkey", + "schema": "public", + "table": "Webhook", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "WebhookExecution_pkey", + "schema": "public", + "table": "WebhookExecution", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "WhatsappBusinessAccount_pkey", + "schema": "public", + "table": "WhatsappBusinessAccount", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "WhatsappCoexistStaging_pkey", + "schema": "public", + "table": "WhatsappCoexistStaging", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "WhatsappFlow_pkey", + "schema": "public", + "table": "WhatsappFlow", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "WhatsappMessageTemplate_pkey", + "schema": "public", + "table": "WhatsappMessageTemplate", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "WhatsappSignupSession_pkey", + "schema": "public", + "table": "WhatsappSignupSession", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "Workspace_pkey", + "schema": "public", + "table": "Workspace", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "WorkspaceApiToken_pkey", + "schema": "public", + "table": "WorkspaceApiToken", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "WorkspaceMac_pkey", + "schema": "public", + "table": "WorkspaceMac", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "WorkspaceMember_pkey", + "schema": "public", + "table": "WorkspaceMember", + "entityType": "pks" + }, + { + "nameExplicit": true, + "columns": [ + "contactInboxId", + "sourceId", + "createdAt" + ], + "nullsNotDistinct": false, + "name": "Message_source_dedup_idx", + "entityType": "uniques", + "schema": "public", + "table": "Message" + }, + { + "nameExplicit": true, + "columns": [ + "workspaceId", + "parentId", + "name" + ], + "nullsNotDistinct": true, + "name": "ProductCategory_workspaceId_parent_name_key", + "entityType": "uniques", + "schema": "public", + "table": "ProductCategory" + }, + { + "nameExplicit": true, + "columns": [ + "token" + ], + "nullsNotDistinct": false, + "name": "UserDeviceToken_token_key", + "entityType": "uniques", + "schema": "public", + "table": "UserDeviceToken" + }, + { + "nameExplicit": true, + "columns": [ + "tokenHash" + ], + "nullsNotDistinct": false, + "name": "WorkspaceApiToken_tokenHash_key", + "entityType": "uniques", + "schema": "public", + "table": "WorkspaceApiToken" + }, + { + "nameExplicit": true, + "columns": [ + "workspaceId", + "periodStart", + "periodEnd" + ], + "nullsNotDistinct": false, + "name": "WorkspaceMac_workspaceId_periodStart_periodEnd_unique", + "entityType": "uniques", + "schema": "public", + "table": "WorkspaceMac" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "nullsNotDistinct": false, + "name": "UserQuota_userId_key", + "schema": "public", + "table": "UserQuota", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "workspaceId" + ], + "nullsNotDistinct": false, + "name": "WorkspaceUsage_workspaceId_key", + "schema": "public", + "table": "WorkspaceUsage", + "entityType": "uniques" + }, + { + "value": "(\"channel\" = 'whatsapp' AND \"integrationWhatsappId\" IS NOT NULL AND \"ctwaClid\" IS NOT NULL AND \"wabaId\" IS NOT NULL) OR (\"channel\" = 'messenger' AND \"integrationMessengerId\" IS NOT NULL) OR (\"channel\" = 'instagram' AND \"integrationInstagramId\" IS NOT NULL)", + "name": "AdsConversionEvent_channel_integration_check", + "entityType": "checks", + "schema": "public", + "table": "AdsConversionEvent" + }, + { + "value": "((\"actorUserId\" IS NOT NULL)::int + (\"actorTokenId\" IS NOT NULL)::int) = 1", + "name": "ConnectSession_actor_exactly_one", + "entityType": "checks", + "schema": "public", + "table": "ConnectSession" + }, + { + "value": "(\"registrationStatus\" <> 'failed' OR \"registrationError\" IS NOT NULL)\n AND (\"registrationStatus\" <> 'registered' OR \"registrationError\" IS NULL)", + "name": "IntegrationWhatsapp_registrationStatus_error_consistent", + "entityType": "checks", + "schema": "public", + "table": "IntegrationWhatsapp" + }, + { + "value": "(\"channel\" = 'whatsapp' AND \"integrationWhatsappId\" IS NOT NULL AND \"integrationMessengerId\" IS NULL AND \"integrationInstagramId\" IS NULL) OR (\"channel\" = 'messenger' AND \"integrationMessengerId\" IS NOT NULL AND \"integrationWhatsappId\" IS NULL AND \"integrationInstagramId\" IS NULL) OR (\"channel\" = 'instagram' AND \"integrationInstagramId\" IS NOT NULL AND \"integrationWhatsappId\" IS NULL AND \"integrationMessengerId\" IS NULL)", + "name": "MessagingAdOperation_channel_integration_check", + "entityType": "checks", + "schema": "public", + "table": "MessagingAdOperation" + }, + { + "value": "(\"channel\" = 'whatsapp' AND \"integrationWhatsappId\" IS NOT NULL AND \"integrationMessengerId\" IS NULL AND \"integrationInstagramId\" IS NULL) OR (\"channel\" = 'messenger' AND \"integrationMessengerId\" IS NOT NULL AND \"integrationWhatsappId\" IS NULL AND \"integrationInstagramId\" IS NULL) OR (\"channel\" = 'instagram' AND \"integrationInstagramId\" IS NOT NULL AND \"integrationWhatsappId\" IS NULL AND \"integrationMessengerId\" IS NULL)", + "name": "MessagingAdsConnection_channel_integration_check", + "entityType": "checks", + "schema": "public", + "table": "MessagingAdsConnection" + }, + { + "value": "\"channel\" IN ('messenger', 'instagram', 'whatsapp')", + "name": "MetaCapiEvent_channel_check", + "entityType": "checks", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "value": "\"actionSource\" IN ('business_messaging', 'email', 'phone_call', 'chat', 'physical_store', 'system_generated', 'other')", + "name": "MetaCapiEvent_actionSource_check", + "entityType": "checks", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "value": "\"contentType\" IN ('product', 'product_group')", + "name": "MetaCapiEvent_contentType_check", + "entityType": "checks", + "schema": "public", + "table": "MetaCapiEvent" + }, + { + "value": "(\"customFieldId\" IS NULL) OR (\"systemFieldKey\" IS NULL)", + "name": "QuestionnaireQuestion_customFieldId_systemFieldKey_exclusive", + "entityType": "checks", + "schema": "public", + "table": "QuestionnaireQuestion" + }, + { + "value": "cardinality(\"candidatePhoneNumberIds\") > 0", + "name": "WhatsappSignupSession_candidates_not_empty", + "entityType": "checks", + "schema": "public", + "table": "WhatsappSignupSession" + } + ], + "renames": [] +} \ No newline at end of file diff --git a/packages/database/package.json b/packages/database/package.json index 497f2accc1..285554160d 100644 --- a/packages/database/package.json +++ b/packages/database/package.json @@ -42,6 +42,7 @@ "make:sharding_migration": "./scripts/make-sharding-migration.sh", "db:migrate-shards": "dotenv -e ../../.env -- tsx scripts/migrate-shards.ts", "backfill:spreadsheet-step-version": "dotenv -e ../../.env -- tsx scripts/backfill-spreadsheet-step-version.ts", + "backfill:connections": "dotenv -e ../../.env -- tsx scripts/backfill-connections.ts", "rotate:encryption-key": "dotenv -e ../../.env -- tsx scripts/rotate-encryption-key.ts", "test": "vitest run --passWithNoTests", "test:db": "dotenv -e ../../.env -- vitest run --dir __tests__/integration", diff --git a/packages/database/scripts/backfill-connections.ts b/packages/database/scripts/backfill-connections.ts new file mode 100644 index 0000000000..8e723adce7 --- /dev/null +++ b/packages/database/scripts/backfill-connections.ts @@ -0,0 +1,438 @@ +/** + * Backfills the `Connection` table from every existing channel (`Inbox` + + * `Integration`) and workspace-level integration row. Idempotent — + * upserts on the table's own `(workspaceId, provider, sourceId)` unique + * constraint, so running it twice in a row is a no-op the second time. + * + * Usage: + * pnpm --filter @chatbotx.io/database backfill:connections -- --dry-run + * pnpm --filter @chatbotx.io/database backfill:connections + * + * Channel rows: status is `disconnected` when the source `Inbox` is + * disconnected, `degraded` when the satellite's `tokenRefreshError` is set + * (channels that track it), else `connected`. `authExpiresAt` comes from + * `auth->'tokens'->>'expiresAt'` when present (OAuth2 channels only). + * + * Workspace-integration rows: `sourceId` is the literal `"workspace"` + * singleton key. `facebookAds.status = 'invalid'` backfills as `degraded`; + * `metaCatalog.deletedAt IS NOT NULL` backfills as `disconnected`. + * + * `outlookCalendar`/`metaConversions`/`chatbotx` are intentionally absent — + * no live table (chatbotx) or no shipped feature yet (outlookCalendar) to + * backfill from. + */ +import { createId } from "@chatbotx.io/utils" +import { sql } from "drizzle-orm" +import { db } from "../src/client" + +const isDryRun = process.argv.includes("--dry-run") + +type ChannelConfig = { + provider: string + channel: string + table: string + /** `null` — the channel has no natural external id; the satellite row's own id is the sourceId. */ + identityColumn: string | null + hasTokenRefreshError: boolean + /** `IntegrationInstagram` backs both `instagram` and `instagramFacebook`, disambiguated by `type`. */ + typeFilter?: string +} + +const CHANNEL_CONFIGS: ChannelConfig[] = [ + { + provider: "api", + channel: "api", + table: "IntegrationApi", + identityColumn: null, + hasTokenRefreshError: false, + }, + { + provider: "messenger", + channel: "messenger", + table: "IntegrationMessenger", + identityColumn: "pageId", + hasTokenRefreshError: true, + }, + { + provider: "whatsapp", + channel: "whatsapp", + table: "IntegrationWhatsapp", + identityColumn: "phoneNumberId", + hasTokenRefreshError: true, + }, + { + provider: "zalo", + channel: "zalo", + table: "IntegrationZalo", + identityColumn: "oaId", + hasTokenRefreshError: true, + }, + { + provider: "telegram", + channel: "telegram", + table: "IntegrationTelegram", + identityColumn: "botId", + hasTokenRefreshError: false, + }, + { + provider: "tiktok", + channel: "tiktok", + table: "IntegrationTiktok", + identityColumn: "openId", + hasTokenRefreshError: true, + }, + { + provider: "smtp", + channel: "smtp", + table: "IntegrationSmtp", + identityColumn: null, + hasTokenRefreshError: false, + }, + { + provider: "webchat", + channel: "webchat", + table: "IntegrationWebchat", + identityColumn: null, + hasTokenRefreshError: false, + }, + { + provider: "instagram", + channel: "instagram", + table: "IntegrationInstagram", + identityColumn: "igId", + hasTokenRefreshError: true, + typeFilter: "instagram", + }, + { + provider: "instagramFacebook", + channel: "instagram", + table: "IntegrationInstagram", + identityColumn: "igId", + hasTokenRefreshError: true, + typeFilter: "facebook", + }, +] + +type WorkspaceConfig = { + provider: string + table: string + authColumn: "auth" | "encryptedAuth" + displayName: string + statusColumn?: string + deletedAtColumn?: string + /** + * Overrides the generic `auth->'tokens'->>'expiresAt'` OAuth2 shape. + * `FacebookAdsAuthValue` is a flat custom shape with `expiresAt` at the + * top level — `IntegrationFacebookAds` also has this precomputed as a + * dedicated `tokenExpiresAt` timestamp column, which this points at + * directly rather than re-deriving from the jsonb `auth` column. + */ + authExpiresAtColumn?: string +} + +const WORKSPACE_CONFIGS: WorkspaceConfig[] = [ + { + provider: "activeCampaign", + table: "IntegrationActiveCampaign", + authColumn: "auth", + displayName: "ActiveCampaign", + }, + { + provider: "drip", + table: "IntegrationDrip", + authColumn: "auth", + displayName: "Drip", + }, + { + provider: "getResponse", + table: "IntegrationGetResponse", + authColumn: "auth", + displayName: "GetResponse", + }, + { + provider: "klaviyo", + table: "IntegrationKlaviyo", + authColumn: "auth", + displayName: "Klaviyo", + }, + { + provider: "mailchimp", + table: "IntegrationMailchimp", + authColumn: "auth", + displayName: "Mailchimp", + }, + { + provider: "mailerLite", + table: "IntegrationMailerLite", + authColumn: "auth", + displayName: "MailerLite", + }, + { + provider: "moosend", + table: "IntegrationMoosend", + authColumn: "auth", + displayName: "Moosend", + }, + { + provider: "sendGrid", + table: "IntegrationSendGrid", + authColumn: "auth", + displayName: "SendGrid", + }, + { + provider: "googleCalendar", + table: "IntegrationGoogleCalendar", + authColumn: "auth", + displayName: "Google Calendar", + }, + { + provider: "googleSheets", + table: "IntegrationGoogleSheet", + authColumn: "auth", + displayName: "Google Sheets", + }, + { + provider: "openai", + table: "IntegrationOpenai", + authColumn: "auth", + displayName: "OpenAI", + }, + { + provider: "openaiCompatible", + table: "IntegrationOpenaiCompatible", + authColumn: "auth", + displayName: "OpenAI-compatible", + }, + { + provider: "claude", + table: "IntegrationClaude", + authColumn: "auth", + displayName: "Claude", + }, + { + provider: "deepseek", + table: "IntegrationDeepseek", + authColumn: "auth", + displayName: "DeepSeek", + }, + { + provider: "gemini", + table: "IntegrationGemini", + authColumn: "auth", + displayName: "Gemini", + }, + { + provider: "openrouter", + table: "IntegrationOpenrouter", + authColumn: "auth", + displayName: "OpenRouter", + }, + { + provider: "facebookAds", + table: "IntegrationFacebookAds", + authColumn: "auth", + displayName: "Facebook Ads", + statusColumn: "status", + // `FacebookAdsAuthValue` is a flat custom shape (`expiresAt` at the top + // level, not nested under `tokens` like the generic OAuth2 shape every + // other workspace-integration config relies on) — read the table's own + // precomputed `tokenExpiresAt` column instead. + authExpiresAtColumn: "tokenExpiresAt", + }, + { + provider: "metaCatalog", + table: "IntegrationMetaCatalog", + authColumn: "encryptedAuth", + displayName: "Meta Catalog", + deletedAtColumn: "deletedAt", + }, +] + +type UpsertInput = { + workspaceId: string + provider: string + kind: "channel" | "integration" + channel: string | null + inboxId: string | null + integrationId: string | null + sourceId: string + displayName: string + status: "connected" | "degraded" | "disconnected" + authExpiresAt: string | null +} + +const upsertConnection = async (input: UpsertInput): Promise => { + if (isDryRun) { + return + } + await db.execute(sql` + INSERT INTO "Connection" + (id, "workspaceId", provider, kind, channel, "inboxId", "integrationId", "sourceId", "displayName", status, "authExpiresAt", "connectedAt", "disconnectedAt") + VALUES + (${createId()}, ${input.workspaceId}, ${input.provider}, ${input.kind}, ${input.channel}, ${input.inboxId}, ${input.integrationId}, ${input.sourceId}, ${input.displayName}, ${input.status}, + ${input.authExpiresAt}::timestamptz, + CASE WHEN ${input.status} = 'disconnected' THEN NULL ELSE now() END, + CASE WHEN ${input.status} = 'disconnected' THEN now() ELSE NULL END) + ON CONFLICT ("workspaceId", provider, "sourceId") DO UPDATE SET + kind = EXCLUDED.kind, + channel = EXCLUDED.channel, + "inboxId" = EXCLUDED."inboxId", + "integrationId" = EXCLUDED."integrationId", + "displayName" = EXCLUDED."displayName", + status = EXCLUDED.status, + "authExpiresAt" = EXCLUDED."authExpiresAt", + "connectedAt" = EXCLUDED."connectedAt", + "disconnectedAt" = EXCLUDED."disconnectedAt" + `) +} +const resolveChannelStatus = ( + inboxStatus: string, + tokenRefreshError: string | null, +): UpsertInput["status"] => { + if (inboxStatus === "disconnected") { + return "disconnected" + } + return tokenRefreshError ? "degraded" : "connected" +} + +const resolveWorkspaceStatus = ( + deletedAt: string | null, + rowStatus: string | null, +): UpsertInput["status"] => { + if (deletedAt) { + return "disconnected" + } + return rowStatus === "invalid" ? "degraded" : "connected" +} + +const backfillChannel = async (config: ChannelConfig): Promise => { + const identityExpr = config.identityColumn + ? sql.raw(`sat."${config.identityColumn}"`) + : sql.raw("sat.id") + const tokenRefreshErrorExpr = config.hasTokenRefreshError + ? sql.raw(`sat."tokenRefreshError"`) + : sql.raw("NULL") + const typeWhere = config.typeFilter + ? sql`WHERE sat.type = ${config.typeFilter}` + : sql`` + + const result = await db.execute<{ + satid: string + workspaceId: string + inboxId: string + name: string + sourceid: string + tokenrefresherror: string | null + inboxstatus: string + authexpiresat: string | null + }>(sql` + SELECT + sat.id AS "satid", + sat."workspaceId" AS "workspaceId", + sat."inboxId" AS "inboxId", + sat.name AS "name", + ${identityExpr} AS "sourceid", + ${tokenRefreshErrorExpr} AS "tokenrefresherror", + inbox.status AS "inboxstatus", + sat.auth->'tokens'->>'expiresAt' AS "authexpiresat" + FROM ${sql.identifier(config.table)} sat + JOIN "Inbox" inbox ON inbox.id = sat."inboxId" + ${typeWhere} + `) + + for (const row of result.rows) { + const status = resolveChannelStatus(row.inboxstatus, row.tokenrefresherror) + + await upsertConnection({ + workspaceId: row.workspaceId, + provider: config.provider, + kind: "channel", + channel: config.channel, + inboxId: row.inboxId, + integrationId: null, + sourceId: row.sourceid, + displayName: row.name, + status, + authExpiresAt: row.authexpiresat, + }) + } + return result.rows.length +} + +const backfillWorkspaceIntegration = async ( + config: WorkspaceConfig, +): Promise => { + const authExpr = sql.raw(`sat."${config.authColumn}"`) + const statusExpr = config.statusColumn + ? sql.raw(`sat."${config.statusColumn}"`) + : sql`NULL` + const deletedAtExpr = config.deletedAtColumn + ? sql.raw(`sat."${config.deletedAtColumn}"`) + : sql`NULL` + const authExpiresAtExpr = config.authExpiresAtColumn + ? sql.raw(`sat."${config.authExpiresAtColumn}"::text`) + : sql`${authExpr}->'tokens'->>'expiresAt'` + + const result = await db.execute<{ + workspaceId: string + integrationId: string + rowstatus: string | null + deletedat: string | null + authexpiresat: string | null + }>(sql` + SELECT + sat."workspaceId" AS "workspaceId", + sat."integrationId" AS "integrationId", + ${statusExpr} AS "rowstatus", + ${deletedAtExpr} AS "deletedat", + ${authExpiresAtExpr} AS "authexpiresat" + FROM ${sql.identifier(config.table)} sat + `) + + for (const row of result.rows) { + const status = resolveWorkspaceStatus(row.deletedat, row.rowstatus) + + await upsertConnection({ + workspaceId: row.workspaceId, + provider: config.provider, + kind: "integration", + channel: null, + inboxId: null, + integrationId: row.integrationId, + sourceId: "workspace", + displayName: config.displayName, + status, + authExpiresAt: row.authexpiresat, + }) + } + return result.rows.length +} + +const main = async (): Promise => { + let total = 0 + + for (const config of CHANNEL_CONFIGS) { + const count = await backfillChannel(config) + total += count + console.log(`${config.provider}: ${count} row(s)`) + } + + for (const config of WORKSPACE_CONFIGS) { + const count = await backfillWorkspaceIntegration(config) + total += count + console.log(`${config.provider}: ${count} row(s)`) + } + + console.log( + isDryRun + ? `Dry run: ${total} connection row(s) would be upserted.` + : `Upserted ${total} connection row(s).`, + ) +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error("Connection backfill failed:", error) + process.exit(1) + }) diff --git a/packages/database/src/partials/connect-session.ts b/packages/database/src/partials/connect-session.ts new file mode 100644 index 0000000000..638642dbd5 --- /dev/null +++ b/packages/database/src/partials/connect-session.ts @@ -0,0 +1,66 @@ +import { z } from "zod" + +/** + * What the client must do next — mirrors `ConnectNextAction` from + * `@chatbotx.io/sdk`. Re-declared here (not imported) because + * `packages/database` cannot depend on `@chatbotx.io/sdk`; the two are kept + * structurally identical by convention, same as `ConnectionKind` between + * `@chatbotx.io/sdk` and `@chatbotx.io/utils`. + */ +export const connectSessionNextActionSchema = z.discriminatedUnion("type", [ + z.object({ type: z.literal("open_url"), url: z.string() }), + z.object({ type: z.literal("show_qr"), qr: z.string() }), + z.object({ + type: z.literal("enter_input"), + inputFields: z.array( + z.object({ + name: z.string(), + type: z.enum(["string", "secret", "number", "boolean", "enum", "url"]), + required: z.boolean(), + labelKey: z.string(), + enumValues: z.array(z.string()).optional(), + description: z.string().optional(), + }), + ), + }), + z.object({ type: z.literal("wait") }), +]) +export type ConnectSessionNextAction = z.infer< + typeof connectSessionNextActionSchema +> + +/** One selectable target surfaced during `awaiting_selection` — no tokens. */ +export const connectSessionTargetSchema = z.object({ + id: z.string(), + name: z.string(), + avatarUrl: z.string().optional(), + selectable: z.boolean(), + disabledReason: z.string().optional(), + alreadyConnected: z.enum(["this_workspace", "other_workspace"]).optional(), +}) +export type ConnectSessionTarget = z.infer + +/** + * Per-target result of a `connectTargets` call. Structurally aligned with + * the `CONNECT_ITEM_STATUSES`/`CONNECT_FAILURE_REASONS` vocabulary in + * `packages/business/src/inbox/connect-outcome-types.ts` (kept in the + * database layer as plain strings — that file cannot be imported here, + * business depends on database, never the reverse). + */ +export const connectSessionOutcomeSchema = z.object({ + targetId: z.string(), + status: z.enum(["connected", "duplicated", "limitReached", "failed"]), + connectionId: z.string().optional(), + reason: z + .enum([ + "notSelectable", + "alreadyConnected", + "channelLimit", + "workspaceLimit", + "providerRejected", + "unknown", + ]) + .optional(), + detail: z.string().optional(), +}) +export type ConnectSessionOutcome = z.infer diff --git a/packages/database/src/partials/connection.ts b/packages/database/src/partials/connection.ts new file mode 100644 index 0000000000..535e2ffad0 --- /dev/null +++ b/packages/database/src/partials/connection.ts @@ -0,0 +1,23 @@ +/** + * `Connection`'s enums are defined in `@chatbotx.io/utils/connection` (same + * rationale as `channelTypes` in `./channel.ts`) so packages that cannot + * depend on the database layer can still reference these values. Re-exported + * here as the conventional database-layer import site. + */ +export { + ACTIVE_CONNECTION_STATUSES, + CONNECTION_TO_INBOX_DISCONNECT_REASON, + type ConnectionKind, + type ConnectionStatus, + type ConnectionStatusReason, + type ConnectSessionErrorCode, + type ConnectSessionPurpose, + type ConnectSessionStatus, + connectionKinds, + connectionStatuses, + connectionStatusReasons, + connectSessionErrorCodes, + connectSessionPurposes, + connectSessionStatuses, + INACTIVE_CONNECTION_STATUSES, +} from "@chatbotx.io/utils/connection" diff --git a/packages/database/src/partials/index.ts b/packages/database/src/partials/index.ts index 66d4d0c0d7..73be81d66a 100644 --- a/packages/database/src/partials/index.ts +++ b/packages/database/src/partials/index.ts @@ -5,6 +5,8 @@ export * from "./automation-throttle" export * from "./broadcast" export * from "./channel" export * from "./coexist-sync-run" +export * from "./connect-session" +export * from "./connection" export * from "./contact" export * from "./contact-on-smart-delay" export * from "./conversation" diff --git a/packages/database/src/partials/workspace-api-token.ts b/packages/database/src/partials/workspace-api-token.ts index 8a7cd0a5a3..af2f1732b4 100644 --- a/packages/database/src/partials/workspace-api-token.ts +++ b/packages/database/src/partials/workspace-api-token.ts @@ -18,8 +18,7 @@ export const workspaceApiTokenScopes = z.enum([ "broadcasts", "analytics", "ecommerce", - "integrations", - "channels", + "connections", "minigames", "appointments", "media", diff --git a/packages/database/src/relations/connect-session.ts b/packages/database/src/relations/connect-session.ts new file mode 100644 index 0000000000..e4baab88b3 --- /dev/null +++ b/packages/database/src/relations/connect-session.ts @@ -0,0 +1,28 @@ +import { defineRelationsPart } from "drizzle-orm" +// biome-ignore lint/performance/noNamespaceImport: drizzle schema +import * as schema from "../schema" + +export const connectSessionRelations = defineRelationsPart(schema, (r) => ({ + connectSessionModel: { + workspace: r.one.workspaceModel({ + from: r.connectSessionModel.workspaceId, + to: r.workspaceModel.id, + optional: false, + }), + targetConnection: r.one.connectionModel({ + from: r.connectSessionModel.targetConnectionId, + to: r.connectionModel.id, + optional: true, + }), + actorUser: r.one.userModel({ + from: r.connectSessionModel.actorUserId, + to: r.userModel.id, + optional: true, + }), + actorToken: r.one.workspaceApiTokenModel({ + from: r.connectSessionModel.actorTokenId, + to: r.workspaceApiTokenModel.id, + optional: true, + }), + }, +})) diff --git a/packages/database/src/relations/connection.ts b/packages/database/src/relations/connection.ts new file mode 100644 index 0000000000..c50f504b96 --- /dev/null +++ b/packages/database/src/relations/connection.ts @@ -0,0 +1,28 @@ +import { defineRelationsPart } from "drizzle-orm" +// biome-ignore lint/performance/noNamespaceImport: drizzle schema +import * as schema from "../schema" + +export const connectionRelations = defineRelationsPart(schema, (r) => ({ + connectionModel: { + workspace: r.one.workspaceModel({ + from: r.connectionModel.workspaceId, + to: r.workspaceModel.id, + optional: false, + }), + inbox: r.one.inboxModel({ + from: r.connectionModel.inboxId, + to: r.inboxModel.id, + optional: true, + }), + integration: r.one.integrationModel({ + from: r.connectionModel.integrationId, + to: r.integrationModel.id, + optional: true, + }), + createdByUser: r.one.userModel({ + from: r.connectionModel.createdBy, + to: r.userModel.id, + optional: true, + }), + }, +})) diff --git a/packages/database/src/relations/inbox.ts b/packages/database/src/relations/inbox.ts index bd0bcc9a2c..8bab41e3fb 100644 --- a/packages/database/src/relations/inbox.ts +++ b/packages/database/src/relations/inbox.ts @@ -53,5 +53,10 @@ export const inboxRelations = defineRelationsPart(schema, (r) => ({ from: r.inboxModel.id, to: r.integrationInstagramModel.inboxId, }), + connection: r.one.connectionModel({ + from: r.inboxModel.id, + to: r.connectionModel.inboxId, + optional: true, + }), }, })) diff --git a/packages/database/src/relations/index.ts b/packages/database/src/relations/index.ts index 6c9751096b..24ec6a6946 100644 --- a/packages/database/src/relations/index.ts +++ b/packages/database/src/relations/index.ts @@ -28,6 +28,8 @@ import { botFieldRelations } from "./bot-field" import { broadcastRelations } from "./broadcast" import { broadcastTargetRelations } from "./broadcast-target" import { coexistSyncRunRelations } from "./coexist-sync-run" +import { connectSessionRelations } from "./connect-session" +import { connectionRelations } from "./connection" import { contactRelations } from "./contact" import { contactCustomFieldRelations } from "./contact-custom-field" import { contactInboxRelations } from "./contact-inbox" @@ -145,6 +147,8 @@ import { workspaceMemberRelations } from "./workspace-member" export const relations = { ...integrationApiRelations, + ...connectionRelations, + ...connectSessionRelations, ...adsConversionEventRelations, ...metaCapiEventRelations, ...messagingAdOperationRelations, diff --git a/packages/database/src/relations/integration.ts b/packages/database/src/relations/integration.ts index bec9757004..36d322bae0 100644 --- a/packages/database/src/relations/integration.ts +++ b/packages/database/src/relations/integration.ts @@ -48,5 +48,9 @@ export const integrationRelations = defineRelationsPart(schema, (r) => ({ from: r.integrationModel.id, to: r.integrationGeminiModel.integrationId, }), + connections: r.many.connectionModel({ + from: r.integrationModel.id, + to: r.connectionModel.integrationId, + }), }, })) diff --git a/packages/database/src/repositories/connect-session/index.ts b/packages/database/src/repositories/connect-session/index.ts new file mode 100644 index 0000000000..b1d08c5baf --- /dev/null +++ b/packages/database/src/repositories/connect-session/index.ts @@ -0,0 +1 @@ +export * from "./repository" diff --git a/packages/database/src/repositories/connect-session/repository.ts b/packages/database/src/repositories/connect-session/repository.ts new file mode 100644 index 0000000000..34fb89ca1f --- /dev/null +++ b/packages/database/src/repositories/connect-session/repository.ts @@ -0,0 +1,129 @@ +import { and, type DatabaseClient, db, eq, sql } from "../../client" +import type { ConnectSessionStatus } from "../../partials/connection" +import { connectSessionModel } from "../../schema" +import type { ConnectSessionModel } from "../../types" + +export const connectSessionRepository = { + async findByIdForWorkspace( + input: { id: string; workspaceId: string }, + tx: DatabaseClient = db, + ): Promise { + return await tx.query.connectSessionModel.findFirst({ + where: { id: input.id, workspaceId: input.workspaceId }, + }) + }, + + /** + * Unscoped lookup by primary key — for internal service methods + * (`attachAuthorization`, `claimTarget`, `recordResults`, …) operating on + * a session already resolved via `findByNonce` earlier in the same flow, + * where the caller has no independently-verified `workspaceId` to scope + * by (the session row itself IS the source of truth for which workspace + * it belongs to). + */ + async findById( + input: { id: string }, + tx: DatabaseClient = db, + ): Promise { + return await tx.query.connectSessionModel.findFirst({ + where: { id: input.id }, + }) + }, + + /** `stateNonceHash` is globally unique — the OAuth callback resolves a session by it alone, before it knows the workspace. */ + async findByStateNonceHash( + input: { stateNonceHash: string }, + tx: DatabaseClient = db, + ): Promise { + return await tx.query.connectSessionModel.findFirst({ + where: { stateNonceHash: input.stateNonceHash }, + }) + }, + + /** + * Enforces the per-workspace pending-session cap + * (`ConnectSessionService.create`) — counts sessions still in an active + * (non-terminal) status AND not yet past `expiresAt`. Without the + * `expiresAt` filter, sessions abandoned mid-flow would count against the + * cap until the `purgeExpired` cron catches up to them, even though every + * reader already treats a past-`expiresAt` row as expired regardless of + * its stored status. + */ + async countActiveByWorkspaceId( + input: { workspaceId: string }, + tx: DatabaseClient = db, + ): Promise { + return await tx.$count( + connectSessionModel, + and( + eq(connectSessionModel.workspaceId, input.workspaceId), + sql`${connectSessionModel.status} IN ('pending', 'authorized', 'awaiting_selection')`, + sql`${connectSessionModel.expiresAt} > now()`, + ), + ) + }, + + async insert( + values: typeof connectSessionModel.$inferInsert, + tx: DatabaseClient = db, + ): Promise { + const [row] = await tx + .insert(connectSessionModel) + .values(values) + .returning() + return row + }, + + async update( + input: { + id: string + values: Partial + }, + tx: DatabaseClient = db, + ): Promise { + const [row] = await tx + .update(connectSessionModel) + .set(input.values) + .where(eq(connectSessionModel.id, input.id)) + .returning() + return row + }, + + async listExpired( + input: { before: Date; statuses: ConnectSessionStatus[] }, + tx: DatabaseClient = db, + ): Promise { + return await tx.query.connectSessionModel.findMany({ + where: { + expiresAt: { lte: input.before }, + status: { in: input.statuses }, + }, + }) + }, + + /** + * Atomic claim of one target id into `claimedTargetIds` — the `WHERE NOT + * (targetId = ANY(...))` clause makes this a compare-and-set at the DB + * level, so two concurrent `connectTargets` calls racing on the same + * target can never both win (the loser sees `claimTarget` return `false` + * and maps that to a `duplicated` outcome instead of double-connecting). + */ + async claimTarget( + input: { id: string; targetId: string }, + tx: DatabaseClient = db, + ): Promise { + const [row] = await tx + .update(connectSessionModel) + .set({ + claimedTargetIds: sql`array_append(${connectSessionModel.claimedTargetIds}, ${input.targetId})`, + }) + .where( + and( + eq(connectSessionModel.id, input.id), + sql`NOT (${input.targetId} = ANY(${connectSessionModel.claimedTargetIds}))`, + ), + ) + .returning({ id: connectSessionModel.id }) + return Boolean(row) + }, +} diff --git a/packages/database/src/repositories/connection/index.ts b/packages/database/src/repositories/connection/index.ts new file mode 100644 index 0000000000..b1d08c5baf --- /dev/null +++ b/packages/database/src/repositories/connection/index.ts @@ -0,0 +1 @@ +export * from "./repository" diff --git a/packages/database/src/repositories/connection/repository.ts b/packages/database/src/repositories/connection/repository.ts new file mode 100644 index 0000000000..6d554b6045 --- /dev/null +++ b/packages/database/src/repositories/connection/repository.ts @@ -0,0 +1,152 @@ +import type { ChannelType } from "@chatbotx.io/utils/channel" +import { type DatabaseClient, db, eq, relationsFilterToSQL } from "../../client" +import type { + ConnectionKind, + ConnectionStatus, +} from "../../partials/connection" +import type { IntegrationType } from "../../partials/integration" +import { connectionModel } from "../../schema" +import type { ConnectionModel } from "../../types" +import { getPaginationWithDefaults } from "../../utils" + +export type ConnectionListInput = { + workspaceId: string + kind?: ConnectionKind | null + provider?: IntegrationType | null + channel?: ChannelType | null + status?: ConnectionStatus[] | null + page?: number | null + perPage?: number | null +} + +const buildWhere = (input: ConnectionListInput) => ({ + workspaceId: input.workspaceId, + kind: input.kind ?? undefined, + provider: input.provider ?? undefined, + channel: input.channel ?? undefined, + status: input.status?.length ? { in: input.status } : undefined, +}) + +export const connectionRepository = { + /** `ORDER BY kind, provider, displayName, id` per the public API contract. */ + async list( + input: ConnectionListInput, + tx: DatabaseClient = db, + ): Promise { + const { limit, offset } = getPaginationWithDefaults(input) + return await tx.query.connectionModel.findMany({ + where: buildWhere(input), + orderBy: { kind: "asc", provider: "asc", displayName: "asc", id: "asc" }, + limit, + offset, + }) + }, + + async count( + input: ConnectionListInput, + tx: DatabaseClient = db, + ): Promise { + return await tx.$count( + connectionModel, + relationsFilterToSQL(connectionModel, buildWhere(input)), + ) + }, + + async findByIdForWorkspace( + input: { id: string; workspaceId: string }, + tx: DatabaseClient = db, + ): Promise { + return await tx.query.connectionModel.findFirst({ + where: { id: input.id, workspaceId: input.workspaceId }, + }) + }, + + /** Revive-or-insert lookup key — `(workspaceId, provider, sourceId)` is the table's unique constraint. */ + async findByProviderSourceId( + input: { workspaceId: string; provider: IntegrationType; sourceId: string }, + tx: DatabaseClient = db, + ): Promise { + return await tx.query.connectionModel.findFirst({ + where: { + workspaceId: input.workspaceId, + provider: input.provider, + sourceId: input.sourceId, + }, + }) + }, + + /** + * Same identity key as `findByProviderSourceId`, without a known + * workspace — used by webhook-triggered `markUnhealthyByIdentifier` + * (a revoked-token webhook payload carries the provider's external id, + * never the workspace). + */ + async findByProviderAndSourceIdAnyWorkspace( + input: { provider: IntegrationType; sourceId: string }, + tx: DatabaseClient = db, + ): Promise { + return await tx.query.connectionModel.findFirst({ + where: { provider: input.provider, sourceId: input.sourceId }, + }) + }, + + async findById( + input: { id: string }, + tx: DatabaseClient = db, + ): Promise { + return await tx.query.connectionModel.findFirst({ + where: { id: input.id }, + }) + }, + + async findByInboxId( + input: { inboxId: string }, + tx: DatabaseClient = db, + ): Promise { + return await tx.query.connectionModel.findFirst({ + where: { inboxId: input.inboxId }, + }) + }, + + async findByIntegrationId( + input: { integrationId: string }, + tx: DatabaseClient = db, + ): Promise { + return await tx.query.connectionModel.findFirst({ + where: { integrationId: input.integrationId }, + }) + }, + + async listDueForRefresh( + input: { before: Date; statuses: ConnectionStatus[] }, + tx: DatabaseClient = db, + ): Promise { + return await tx.query.connectionModel.findMany({ + where: { + status: { in: input.statuses }, + authExpiresAt: { lte: input.before }, + }, + orderBy: { authExpiresAt: "asc" }, + }) + }, + + async insert( + values: typeof connectionModel.$inferInsert, + tx: DatabaseClient = db, + ): Promise { + const [row] = await tx.insert(connectionModel).values(values).returning() + return row + }, + + async update( + input: { id: string; values: Partial }, + tx: DatabaseClient = db, + ): Promise { + const [row] = await tx + .update(connectionModel) + .set(input.values) + .where(eq(connectionModel.id, input.id)) + .returning() + return row + }, +} diff --git a/packages/database/src/repositories/index.ts b/packages/database/src/repositories/index.ts index 9d73243577..19a0c44167 100644 --- a/packages/database/src/repositories/index.ts +++ b/packages/database/src/repositories/index.ts @@ -13,6 +13,8 @@ export * from "./broadcast" export * from "./broadcast-purge" export * from "./coexist-sync-run" export * from "./condition" +export * from "./connect-session" +export * from "./connection" export * from "./contact" export * from "./contact-custom-field" export * from "./contact-inbox" diff --git a/packages/database/src/schema/connect-session.ts b/packages/database/src/schema/connect-session.ts new file mode 100644 index 0000000000..9a335d47ac --- /dev/null +++ b/packages/database/src/schema/connect-session.ts @@ -0,0 +1,112 @@ +import type { EncryptedData } from "@chatbotx.io/encryption" +import { sql } from "drizzle-orm" +import { + check, + index, + jsonb, + pgTable, + text, + timestamp, + uniqueIndex, +} from "drizzle-orm/pg-core" +import type { + ConnectSessionNextAction, + ConnectSessionOutcome, + ConnectSessionTarget, +} from "../partials/connect-session" +import type { + ConnectSessionErrorCode, + ConnectSessionPurpose, + ConnectSessionStatus, +} from "../partials/connection" +import type { IntegrationType } from "../partials/integration" +import { + bigintAsString, + sharedColumns, + timestampConfig, +} from "../partials/shared" +import { userModel } from "./auth-user" +import { connectionModel } from "./connection" +import { workspaceModel } from "./workspace" +import { workspaceApiTokenModel } from "./workspace-api-token" + +/** + * Strategy-agnostic, multi-step connect-flow record (modelled on Home + * Assistant config flows / Nango connect sessions) — a future QR-code, + * device-code, OAuth1, or "enter verification PIN" strategy is a new + * `nextAction.type`, not a new table. Generalises `WhatsappSignupSession` + * (kept, wrapped — its id goes in `encryptedAuth`) and the three + * `fb_*_pending_auth` cookies (`apps/builder/src/lib/facebook-pending-auth.ts`). + */ +export const connectSessionModel = pgTable( + "ConnectSession", + { + ...sharedColumns, + workspaceId: bigintAsString() + .notNull() + .references(() => workspaceModel.id, { + onDelete: "cascade", + onUpdate: "cascade", + }), + provider: text().$type().notNull(), + purpose: text().$type().notNull(), + // For `reconnect`: identity must match the existing Connection on completion. + targetConnectionId: bigintAsString().references(() => connectionModel.id, { + onDelete: "set null", + onUpdate: "cascade", + }), + // Exactly one of actorUserId/actorTokenId is set — enforced by the CHECK + // constraint below. A token-actor session's Connection rows are created + // with `createdBy = null` (repo convention for token-created rows). + actorUserId: bigintAsString().references(() => userModel.id, { + onDelete: "set null", + onUpdate: "cascade", + }), + actorTokenId: bigintAsString().references(() => workspaceApiTokenModel.id, { + onDelete: "set null", + onUpdate: "cascade", + }), + // White-label: the credential owner and host that started the flow. + platformOwnerId: text(), + originHost: text(), + // Validated with `sanitizeReferer` (`apps/builder/src/lib/oauth-referer.ts`). + returnUrl: text(), + // SHA-256 hex digest of a 32-byte nonce; plaintext appears once inside `authorizeUrl`. + stateNonceHash: text().notNull(), + status: text().$type().notNull().default("pending"), + // Provider-defined step name (`authorize`, `select`, `verify_code`, …). + step: text().notNull().default("authorize"), + nextAction: jsonb().$type(), + encryptedAuth: jsonb().$type(), + targets: jsonb().$type().default(sql`[]`).notNull(), + claimedTargetIds: text().array().default(sql`[]`).notNull(), + resultConnectionIds: text().array().default(sql`[]`).notNull(), + results: jsonb() + .$type() + .default(sql`[]`) + .notNull(), + errorCode: text().$type(), + // 10 min while pending, 30 min once authorized. Reads treat + // `expiresAt <= now()` as `expired` regardless of the stored `status`. + expiresAt: timestamp(timestampConfig).notNull(), + consumedAt: timestamp(timestampConfig), + }, + (table) => [ + index("ConnectSession_workspaceId_idx").using( + "btree", + table.workspaceId.asc().nullsLast(), + ), + index("ConnectSession_expiresAt_idx").using( + "btree", + table.expiresAt.asc().nullsLast(), + ), + uniqueIndex("ConnectSession_stateNonceHash_key").using( + "btree", + table.stateNonceHash.asc().nullsLast(), + ), + check( + "ConnectSession_actor_exactly_one", + sql`(("actorUserId" IS NOT NULL)::int + ("actorTokenId" IS NOT NULL)::int) = 1`, + ), + ], +) diff --git a/packages/database/src/schema/connection.ts b/packages/database/src/schema/connection.ts new file mode 100644 index 0000000000..a8fadbeabe --- /dev/null +++ b/packages/database/src/schema/connection.ts @@ -0,0 +1,101 @@ +import type { ChannelType } from "@chatbotx.io/utils/channel" +import { + index, + pgTable, + text, + timestamp, + uniqueIndex, +} from "drizzle-orm/pg-core" +import type { + ConnectionKind, + ConnectionStatus, + ConnectionStatusReason, +} from "../partials/connection" +import type { IntegrationType } from "../partials/integration" +import { + bigintAsString, + sharedColumns, + timestampConfig, +} from "../partials/shared" +import { userModel } from "./auth-user" +import { inboxModel } from "./inbox" +import { integrationModel } from "./integration-base" +import { workspaceModel } from "./workspace" + +/** + * Unified Connection domain: one row per `(workspaceId, provider, sourceId)` + * — revive-or-insert, never duplicate, matching `Inbox` semantics. + * `channel`/`inboxId` are set iff `kind = "channel"`; `integrationId` is set + * for workspace-level integrations and survives satellite delete as + * `disconnected` (`onDelete: "set null"`). See + * `packages/business/src/connection/state.ts` for the status state machine + * this table's `status`/`statusReason` columns drive. + */ +export const connectionModel = pgTable( + "Connection", + { + ...sharedColumns, + workspaceId: bigintAsString() + .notNull() + .references(() => workspaceModel.id, { + onDelete: "cascade", + onUpdate: "cascade", + }), + provider: text().$type().notNull(), + kind: text().$type().notNull(), + channel: text().$type(), + inboxId: bigintAsString().references(() => inboxModel.id, { + onDelete: "cascade", + onUpdate: "cascade", + }), + integrationId: bigintAsString().references(() => integrationModel.id, { + onDelete: "set null", + onUpdate: "cascade", + }), + // pageId / phoneNumberId / igId / oaId / botId / openId / "workspace" for singletons. + sourceId: text().notNull(), + displayName: text().notNull(), + status: text().$type().notNull().default("connected"), + statusReason: text().$type(), + lastError: text(), + authExpiresAt: timestamp(timestampConfig), + createdBy: bigintAsString().references(() => userModel.id, { + onDelete: "set null", + onUpdate: "cascade", + }), + connectedAt: timestamp(timestampConfig), + disconnectedAt: timestamp(timestampConfig), + }, + (table) => [ + index("Connection_workspaceId_idx").using( + "btree", + table.workspaceId.asc().nullsLast(), + ), + uniqueIndex("Connection_workspaceId_provider_sourceId_key").using( + "btree", + table.workspaceId.asc().nullsLast(), + table.provider.asc().nullsLast(), + table.sourceId.asc().nullsLast(), + ), + uniqueIndex("Connection_inboxId_key").using( + "btree", + table.inboxId.asc().nullsLast(), + ), + // Workspace-level integrations are 1:1 with their Integration row, but + // multiple Connection rows may share `integrationId = NULL` (channels). + // A plain unique index on a nullable column already allows multiple + // NULL rows in Postgres (NULLs are never equal to each other) — no + // partial/WHERE clause needed. Channel connections (integrationId NULL) + // are unconstrained here; workspace-level integrations (integrationId + // set) are enforced 1:1 with their Integration row. + uniqueIndex("Connection_integrationId_key").using( + "btree", + table.integrationId.asc().nullsLast(), + ), + index("Connection_status_authExpiresAt_idx").using( + "btree", + table.status.asc().nullsLast(), + table.authExpiresAt.asc().nullsLast(), + ), + ], +) diff --git a/packages/database/src/schema/index.ts b/packages/database/src/schema/index.ts index eb631b28f1..896f912dbd 100644 --- a/packages/database/src/schema/index.ts +++ b/packages/database/src/schema/index.ts @@ -37,6 +37,8 @@ export * from "./bot-field" export * from "./broadcast" export * from "./broadcast-target" export * from "./coexist-sync-run" +export * from "./connect-session" +export * from "./connection" export * from "./contact" export * from "./contact-active-hourly" export * from "./contact-active-monthly" diff --git a/packages/database/src/types.ts b/packages/database/src/types.ts index 55cbe7956f..c90496843d 100644 --- a/packages/database/src/types.ts +++ b/packages/database/src/types.ts @@ -1,5 +1,7 @@ import type * as schema from "./schema" +export type ConnectionModel = typeof schema.connectionModel.$inferSelect +export type ConnectSessionModel = typeof schema.connectSessionModel.$inferSelect export type IntegrationWebchatModel = typeof schema.integrationWebchatModel.$inferSelect export type IntegrationApiModel = typeof schema.integrationApiModel.$inferSelect diff --git a/packages/sdk/__tests__/ensure-fresh-auth.test.ts b/packages/sdk/__tests__/ensure-fresh-auth.test.ts new file mode 100644 index 0000000000..515731a00d --- /dev/null +++ b/packages/sdk/__tests__/ensure-fresh-auth.test.ts @@ -0,0 +1,135 @@ +import { describe, expect, test, vi } from "vitest" +import { + AuthRefreshException, + AuthType, + type Context, + Integration, + type IntegrationDefinition, + type Oauth2AuthValue, +} from "../src" + +const baseAuth = (expiresAt: string): Oauth2AuthValue => ({ + authType: AuthType.oauth2, + clientId: "client-1", + clientSecret: "secret-1", + redirectUrl: "https://example.com/callback", + tokens: { accessToken: "token-1", expiresAt }, +}) + +type RefreshAuthFn = (props: { + auth: Oauth2AuthValue +}) => Promise + +const makeIntegration = (refreshAuth: RefreshAuthFn) => + new Integration< + IntegrationDefinition, Oauth2AuthValue> + >({ + name: "fixture", + actions: {}, + handleRequest: async () => "ok", + disconnect: async () => undefined, + refreshAuth, + }) + +const makeContext = (auth: Oauth2AuthValue) => { + const save = vi.fn(async () => undefined) + const markOffline = vi.fn(async () => undefined) + const ctx: Context = { + storagePrefix: "test", + auth, + authStore: { load: async () => auth, save, markOffline }, + platform: { + appUrl: "https://app.test", + wsUrl: "wss://app.test", + storageUrl: "https://storage.test", + getRealtimeAuthHeaders: async () => ({}), + }, + } + return { ctx, save, markOffline } +} + +describe("Integration.ensureFreshAuth", () => { + test("no-ops when the token is far from expiry and force is not set", async () => { + const refreshAuth = vi.fn() + const integration = makeIntegration(refreshAuth) + const farFuture = new Date(Date.now() + 60 * 60 * 1000).toISOString() + const { ctx, save } = makeContext(baseAuth(farFuture)) + + const result = await integration.ensureFreshAuth(ctx) + + expect(refreshAuth).not.toHaveBeenCalled() + expect(save).not.toHaveBeenCalled() + expect(result.auth.tokens.expiresAt).toBe(farFuture) + }) + + test("refreshes and persists when within the proactive-refresh window", async () => { + const newAuth = baseAuth( + new Date(Date.now() + 60 * 60 * 1000).toISOString(), + ) + const refreshAuth = vi.fn(async () => newAuth) + const integration = makeIntegration(refreshAuth) + const soon = new Date(Date.now() + 60 * 1000).toISOString() + const { ctx, save } = makeContext(baseAuth(soon)) + + const result = await integration.ensureFreshAuth(ctx) + + expect(refreshAuth).toHaveBeenCalledTimes(1) + expect(save).toHaveBeenCalledWith(newAuth) + expect(result.auth).toBe(newAuth) + }) + + test("force:true refreshes even when the token is far from expiry", async () => { + const newAuth = baseAuth( + new Date(Date.now() + 2 * 60 * 60 * 1000).toISOString(), + ) + const refreshAuth = vi.fn(async () => newAuth) + const integration = makeIntegration(refreshAuth) + const farFuture = new Date(Date.now() + 60 * 60 * 1000).toISOString() + const { ctx, save } = makeContext(baseAuth(farFuture)) + + const result = await integration.ensureFreshAuth(ctx, { force: true }) + + expect(refreshAuth).toHaveBeenCalledTimes(1) + expect(save).toHaveBeenCalledWith(newAuth) + expect(result.auth).toBe(newAuth) + }) + + test("a custom withinMs widens the proactive-refresh window beyond the default buffer", async () => { + const newAuth = baseAuth( + new Date(Date.now() + 60 * 60 * 1000).toISOString(), + ) + const refreshAuth = vi.fn(async () => newAuth) + const integration = makeIntegration(refreshAuth) + // 20 minutes out — outside the SDK's default 5-minute buffer, so a bare + // `ensureFreshAuth(ctx)` would NOT refresh this; a 30-minute window does. + const twentyMinutesOut = new Date(Date.now() + 20 * 60 * 1000).toISOString() + const { ctx, save } = makeContext(baseAuth(twentyMinutesOut)) + + const untouched = await integration.ensureFreshAuth(ctx) + expect(refreshAuth).not.toHaveBeenCalled() + expect(untouched.auth.tokens.expiresAt).toBe(twentyMinutesOut) + + const refreshed = await integration.ensureFreshAuth(ctx, { + withinMs: 30 * 60 * 1000, + }) + expect(refreshAuth).toHaveBeenCalledTimes(1) + expect(save).toHaveBeenCalledWith(newAuth) + expect(refreshed.auth).toBe(newAuth) + }) + + test("marks the connection offline and throws on terminal refresh failure", async () => { + class FixtureAuthException extends Error {} + const refreshAuth: RefreshAuthFn = () => { + throw new FixtureAuthException("revoked") + } + const integration = makeIntegration(refreshAuth) + const { ctx, markOffline } = makeContext( + baseAuth(new Date(Date.now() + 60 * 60 * 1000).toISOString()), + ) + + await expect( + integration.ensureFreshAuth(ctx, { force: true }), + ).rejects.toBeInstanceOf(AuthRefreshException) + expect(markOffline).toHaveBeenCalledTimes(1) + }) +}) diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index bb411cc8cc..bdf689a7b6 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -2,6 +2,7 @@ export * from "./lib/action" export * from "./lib/auth" export * from "./lib/channel-error" export * from "./lib/channel-error-codes" +export * from "./lib/connection" export * from "./lib/exception" export * from "./lib/exceptions" export * from "./lib/flow-step-data" diff --git a/packages/sdk/src/lib/connection.ts b/packages/sdk/src/lib/connection.ts new file mode 100644 index 0000000000..dc47c15d18 --- /dev/null +++ b/packages/sdk/src/lib/connection.ts @@ -0,0 +1,126 @@ +import type { AuthValue } from "./auth" +import type { Handler } from "./shared" + +/** + * How a connection is established. Kept as an open string-literal union so a + * future strategy (QR login, device code, OAuth1, "paste callback URL") is + * additive — a new literal here plus a new `ConnectNextAction.type`, never a + * new table or a breaking change to `ConnectionProvider`. + */ +export type ConnectionStrategy = + | "oauth_redirect" + | "oauth_popup" + | "token" + | "api_key" + | "self_serve" + +/** One field of a provider's `config` (credential-strategy connect) or a post-connect action's `input`. */ +export type ConnectionConfigField = { + name: string + type: "string" | "secret" | "number" | "boolean" | "enum" | "url" + required: boolean + labelKey: string + enumValues?: readonly string[] + description?: string +} + +/** What the client must do next to continue a connect session. */ +export type ConnectNextAction = + | { type: "open_url"; url: string } + | { type: "show_qr"; qr: string } + | { type: "enter_input"; inputFields: ConnectionConfigField[] } + | { type: "wait" } + +export type ConnectionKind = "channel" | "integration" | "sub_connection" + +export type ConnectionHealth = + | { ok: true; authExpiresAt?: string } + | { ok: false; revoked: boolean; error: string } + +/** Human-facing identity of a connected account, derived from its auth/profile data. */ +export type ConnectionDescriptor = { + sourceId: string + displayName: string + authExpiresAt?: string + avatarUrl?: string +} + +/** One selectable target surfaced by `listCandidates` during a connect session — carries auth, never persisted as-is. */ +export type ConnectionCandidate = ConnectionDescriptor & { + alreadyConnected?: "this_workspace" | "other_workspace" + auth: AuthValue +} + +/** Opaque platform-credential input threaded through `authorizeUrl`/`exchangeCode` — shape is provider-specific, resolved by the business layer. */ +export type ConnectionCredential = unknown + +/** + * Per-provider adapter the Connection domain drives to authorize, describe, + * verify, and tear down a connection. `IAuth` is the provider's `AuthValue` + * shape; `ICreds` is the shape of `config` for `token`/`api_key`/`self_serve` + * strategies (never set for OAuth strategies). + */ +export type ConnectionProvider< + IAuth extends AuthValue = AuthValue, + // biome-ignore lint/suspicious/noExplicitAny: strategy-dependent credential shape + ICreds = any, +> = { + kind: ConnectionKind + strategy: ConnectionStrategy + multiAccount: boolean + /** Drives `listConnectionProviders` and validates `config` for credential strategies. */ + configFields: readonly ConnectionConfigField[] + describe: (auth: IAuth) => ConnectionDescriptor + authorizeUrl?: (i: { + credential: ConnectionCredential + callbackUrl: string + state: string + }) => string + /** + * Returns `AuthValue`, not `IAuth` — for a single-account provider these + * coincide, but a multi-account provider (Messenger Business Login) gets + * back a *user*-level token here that is not yet any specific page's + * `IAuth` (it has no `pageId` to satisfy `IAuth`'s narrower `metadata` + * shape). `listCandidates` receives this same session-level value and is + * what derives each candidate's own final `IAuth`; `describe`/`verify`/ + * `webhook`/`candidateToConfig` never see it. + */ + exchangeCode?: Handler< + { code: string; callbackUrl: string; credential: ConnectionCredential }, + AuthValue + > + listCandidates?: Handler<{ auth: AuthValue }, ConnectionCandidate[]> + /** + * Extra satellite-table columns a candidate's own `auth` carries beyond + * `describe()`'s `{sourceId, displayName}` and the generic auth/tokens + * shape — e.g. Instagram's `username` (its `auth.metadata` already has + * `igId`/`pageId`, both covered by `describe()`/`identityColumn`, but the + * satellite table's `username` column has no default and isn't part of + * either). Read generically by `ConnectionService.connectTargets` and + * passed straight through to `ConnectionStoreBinding.insertRow`'s + * `config` — omit when a candidate's own `auth`/`descriptor` already + * cover every NOT NULL column with no default. + */ + candidateToConfig?: (auth: IAuth) => Record + /** Validates `token`/`api_key`/`self_serve` config with a live provider call. */ + fromCredentials?: Handler + verify: Handler<{ auth: IAuth }, ConnectionHealth> + isRevokedTokenError: (error: unknown) => boolean + webhook?: { + subscribe: Handler<{ auth: IAuth }, void> + unsubscribe: Handler<{ auth: IAuth }, void> + } + /** + * Reserved for provider-specific post-connect verbs (WhatsApp + * requestVerificationCode/verifyCode, Telegram setWebhook, …), exposed + * later as `POST /v1/connections/{id}/actions/{name}`. Typed now so a + * provider can start declaring one; no route consumes this in Phase 0-3. + */ + actions?: Record< + string, + { + inputFields: readonly ConnectionConfigField[] + run: Handler<{ auth: IAuth; input: Record }, unknown> + } + > +} diff --git a/packages/sdk/src/lib/integration.ts b/packages/sdk/src/lib/integration.ts index c26efdc266..b39bd9223c 100644 --- a/packages/sdk/src/lib/integration.ts +++ b/packages/sdk/src/lib/integration.ts @@ -1,5 +1,6 @@ import type { MetadataPayload } from "@chatbotx.io/flow-config" import type { AuthValue, Oauth2AuthValue } from "./auth" +import type { ConnectionProvider } from "./connection" import { AuthException, AuthRefreshException, @@ -388,6 +389,14 @@ export type IntegrationDefinition< > disconnect: Handler refreshAuth?: Handler<{ auth: IAuth }, IAuth> + /** + * Connection-domain adapter (connect/verify/webhook/describe) driving the + * unified `Connection` lifecycle. Optional during the Phase 0-3 rollout — + * a provider with no `connection` block simply has no `CONNECTION_REGISTRY` + * entry yet (`@chatbotx.io/connections` maps it to `null`). + */ + // biome-ignore lint/suspicious/noExplicitAny: credential shape varies per provider + connection?: ConnectionProvider } // --------------------------------------------------------------------------- @@ -469,6 +478,10 @@ export class Integration< return this.props.disconnect } + get connection(): T["connection"] { + return this.props.connection + } + get handleRequest(): T["handleRequest"] { return this.props.handleRequest } @@ -567,7 +580,10 @@ export class Integration< } } - private shouldProactivelyRefresh(auth: AuthValue): boolean { + private shouldProactivelyRefresh( + auth: AuthValue, + withinMs: number = AUTH_REFRESH_BUFFER_MS, + ): boolean { if (!this.props.refreshAuth || auth.authType !== "oauth2") { return false } @@ -579,11 +595,12 @@ export class Integration< if (Number.isNaN(expiresAtMs)) { return false } - return expiresAtMs - Date.now() < AUTH_REFRESH_BUFFER_MS + return expiresAtMs - Date.now() < withinMs } private async refreshAndPersist( ctx: Context, + opts?: { withinMs?: number; force?: boolean }, ): Promise> { const refreshAuth = this.props.refreshAuth if (!refreshAuth) { @@ -604,7 +621,11 @@ export class Integration< } } - if (!this.shouldProactivelyRefresh(baseAuth)) { + if ( + !( + opts?.force || this.shouldProactivelyRefresh(baseAuth, opts?.withinMs) + ) + ) { return { ...ctx, auth: baseAuth } } @@ -618,6 +639,21 @@ export class Integration< return ctx.authStore?.withLock ? await ctx.authStore.withLock(run) : run() } + /** + * Public entry point for the Connection domain to force or proactively + * refresh auth outside the request-triggered `invokeWithRefresh` flow + * (the `refresh-connections` cron, `POST /v1/connections/{id}/refresh`). + * With no `opts`, behaves like the proactive check used before every + * handler call (refresh only when within {@link AUTH_REFRESH_BUFFER_MS} of + * expiry). `force: true` always calls `refreshAuth` regardless of expiry. + */ + async ensureFreshAuth( + ctx: Context, + opts?: { withinMs?: number; force?: boolean }, + ): Promise> { + return await this.refreshAndPersist(ctx, opts) + } + /** * Call the per-integration `refreshAuth` with bounded exponential backoff. * On terminal failure (refresh-token revoked, all retries exhausted) the diff --git a/packages/sdk/src/lib/shared/context.ts b/packages/sdk/src/lib/shared/context.ts index 817817d15a..b9c778d16d 100644 --- a/packages/sdk/src/lib/shared/context.ts +++ b/packages/sdk/src/lib/shared/context.ts @@ -1,5 +1,6 @@ import type { Readable } from "node:stream" import type { AuthValue } from "../auth" +import type { ConnectionHealth } from "../connection" export type ContextUploader = { putObject( @@ -32,6 +33,12 @@ export type AuthStore = { * failure. */ markOffline?: (reason?: unknown) => Promise + /** + * Optional hook for a live health check result (`ConnectionProvider.verify`) + * to update the connection's displayed status independently of a refresh + * cycle — e.g. `POST /v1/connections/{id}/verify`. + */ + recordHealth?: (health: ConnectionHealth) => Promise } export type Context> = { diff --git a/packages/utils/package.json b/packages/utils/package.json index 2d8cd1455a..479d2b80ec 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -20,6 +20,7 @@ ".": "./src/index.ts", "./broadcast": "./src/broadcast.ts", "./channel": "./src/channel.ts", + "./connection": "./src/connection.ts", "./contact-filter-value-format": "./src/contact-filter-value-format.ts", "./conversation": "./src/conversation.ts", "./crypto": "./src/crypto.ts", diff --git a/packages/utils/src/connection.ts b/packages/utils/src/connection.ts new file mode 100644 index 0000000000..1d24dda74b --- /dev/null +++ b/packages/utils/src/connection.ts @@ -0,0 +1,117 @@ +import { z } from "zod" + +/** + * The three connection families driving the `Connection.status` state + * machine (`packages/business/src/connection/state.ts`). + * + * ACTIVE = `connected | degraded` — quota is held, `Inbox.status = connected`. + * INACTIVE = `needs_reauth | paused | disconnected` — quota is released, + * `Inbox.status = disconnected`. Quota is consumed exactly on an + * INACTIVE/absent → ACTIVE edge and released exactly on ACTIVE → INACTIVE, + * nowhere else. + */ +export const connectionStatuses = z.enum([ + "connected", + "degraded", + "needs_reauth", + "paused", + "disconnected", +]) +export type ConnectionStatus = z.infer + +export const ACTIVE_CONNECTION_STATUSES: readonly ConnectionStatus[] = [ + "connected", + "degraded", +] +export const INACTIVE_CONNECTION_STATUSES: readonly ConnectionStatus[] = [ + "needs_reauth", + "paused", + "disconnected", +] + +/** Why a connection last moved into (or stayed in) a non-`connected` status. */ +export const connectionStatusReasons = z.enum([ + "manual", + "workspace_purge", + "trial_expired", + "tenant_suspended", + "token_revoked", + "provider_revoked", + "refresh_failed", + "verify_failed", + "quota_exceeded", + "orphaned_webhook", +]) +export type ConnectionStatusReason = z.infer + +/** + * Legacy mirror for `Inbox.disconnectReason` — narrower than + * `ConnectionStatusReason`. Kept in sync manually (not derived) because the + * two enums serve different audiences: `Connection` reasons are precise for + * the API/audit trail, `Inbox.disconnectReason` predates this domain and its + * values are already load-bearing (UI copy, exports). + */ +export const CONNECTION_TO_INBOX_DISCONNECT_REASON: Record< + ConnectionStatusReason, + "manual" | "token_revoked" +> = { + manual: "manual", + workspace_purge: "manual", + trial_expired: "manual", + tenant_suspended: "manual", + token_revoked: "token_revoked", + provider_revoked: "token_revoked", + refresh_failed: "token_revoked", + verify_failed: "manual", + quota_exceeded: "manual", + orphaned_webhook: "manual", +} + +/** + * Same three-way split `packages/sdk`'s `ConnectionKind` uses, re-declared + * here as a Zod enum (same rationale as `channelTypes`) so the database layer + * and public API schemas can validate against it without depending on the + * SDK package. `sub_connection` is reserved — no reader/writer uses it yet. + */ +export const connectionKinds = z.enum([ + "channel", + "integration", + "sub_connection", +]) +export type ConnectionKind = z.infer + +/** `ConnectSession.purpose` — set server-side, never accepted from client input. */ +export const connectSessionPurposes = z.enum([ + "connect", + "reconnect", + "facebook_ads", + "messaging_ads", + "lead_ads", + "meta_catalog", +]) +export type ConnectSessionPurpose = z.infer + +/** `ConnectSession.status` lifecycle. `expiresAt <= now()` reads as `expired` regardless of the stored value. */ +export const connectSessionStatuses = z.enum([ + "pending", + "authorized", + "awaiting_selection", + "completed", + "failed", + "expired", + "cancelled", +]) +export type ConnectSessionStatus = z.infer + +export const connectSessionErrorCodes = z.enum([ + "state_mismatch", + "expired", + "provider_denied", + "provider_error", + "no_candidates", + "already_connected", + "quota_exceeded", + "trial_expired", + "internal_error", +]) +export type ConnectSessionErrorCode = z.infer diff --git a/packages/worker-config/src/queues/schedule/index.ts b/packages/worker-config/src/queues/schedule/index.ts index 81dc646230..efb3028a65 100644 --- a/packages/worker-config/src/queues/schedule/index.ts +++ b/packages/worker-config/src/queues/schedule/index.ts @@ -30,6 +30,7 @@ export const ScheduleJobData = { reconcileMetaCatalogSyncs: "reconcileMetaCatalogSyncs", purgeCoexistStaging: "purgeCoexistStaging", purgeWhatsappSignupSessions: "purgeWhatsappSignupSessions", + purgeExpiredConnectSessions: "purgeExpiredConnectSessions", purgeWorkspaces: "purgeWorkspaces", clearExpiredSupportAccess: "clearExpiredSupportAccess", purgeBroadcasts: "purgeBroadcasts", @@ -185,6 +186,11 @@ export type ScheduleJobPurgeWhatsappSignupSessions = { data: Record } +export type ScheduleJobPurgeExpiredConnectSessions = { + type: typeof ScheduleJobData.purgeExpiredConnectSessions + data: Record +} + export type ScheduleJobPurgeWorkspaces = { type: typeof ScheduleJobData.purgeWorkspaces data: Record @@ -253,6 +259,7 @@ export type ScheduleJobData = | ScheduleJobReconcileMetaCatalogSyncs | ScheduleJobPurgeCoexistStaging | ScheduleJobPurgeWhatsappSignupSessions + | ScheduleJobPurgeExpiredConnectSessions | ScheduleJobPurgeWorkspaces | ScheduleJobClearExpiredSupportAccess | ScheduleJobPurgeBroadcasts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a51b043f1a..4c34583fc5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -89,6 +89,9 @@ importers: '@chatbotx.io/business': specifier: workspace:* version: link:../../packages/business + '@chatbotx.io/connections': + specifier: workspace:* + version: link:../../packages/connections '@chatbotx.io/database': specifier: workspace:* version: link:../../packages/database @@ -682,6 +685,9 @@ importers: '@chatbotx.io/business': specifier: workspace:* version: link:../../packages/business + '@chatbotx.io/connections': + specifier: workspace:* + version: link:../../packages/connections '@chatbotx.io/database': specifier: workspace:* version: link:../../packages/database @@ -2231,6 +2237,9 @@ importers: jose: specifier: ^6.2.3 version: 6.2.3 + ky: + specifier: ^2.0.2 + version: 2.0.2 libphonenumber-js: specifier: ^1.12.42 version: 1.13.6 @@ -2266,6 +2275,109 @@ importers: specifier: ^4.1.0 version: 4.1.8(@opentelemetry/api@1.9.1)(@types/node@24.13.1)(@vitest/coverage-v8@4.1.8)(jsdom@25.0.1)(msw@2.14.6(@types/node@24.13.1)(typescript@5.9.3))(vite@7.3.5(@types/node@24.13.1)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.23.0)(yaml@2.9.0)) + packages/connections: + dependencies: + '@chatbotx.io/business': + specifier: workspace:* + version: link:../business + '@chatbotx.io/database': + specifier: workspace:* + version: link:../database + '@chatbotx.io/encryption': + specifier: workspace:* + version: link:../encryption + '@chatbotx.io/integration-active-campaign': + specifier: workspace:* + version: link:../../integrations/active-campaign + '@chatbotx.io/integration-api': + specifier: workspace:* + version: link:../../integrations/api + '@chatbotx.io/integration-chatbotx': + specifier: workspace:* + version: link:../../integrations/chatbotx + '@chatbotx.io/integration-drip': + specifier: workspace:* + version: link:../../integrations/drip + '@chatbotx.io/integration-facebook-ads': + specifier: workspace:* + version: link:../../integrations/facebook-ads + '@chatbotx.io/integration-get-response': + specifier: workspace:* + version: link:../../integrations/get-response + '@chatbotx.io/integration-google-calendar': + specifier: workspace:* + version: link:../../integrations/google-calendar + '@chatbotx.io/integration-google-sheets': + specifier: workspace:* + version: link:../../integrations/google-sheets + '@chatbotx.io/integration-instagram': + specifier: workspace:* + version: link:../../integrations/instagram + '@chatbotx.io/integration-instagram-facebook': + specifier: workspace:* + version: link:../../integrations/instagram-facebook + '@chatbotx.io/integration-klaviyo': + specifier: workspace:* + version: link:../../integrations/klaviyo + '@chatbotx.io/integration-mailchimp': + specifier: workspace:* + version: link:../../integrations/mailchimp + '@chatbotx.io/integration-mailer-lite': + specifier: workspace:* + version: link:../../integrations/mailer-lite + '@chatbotx.io/integration-messenger': + specifier: workspace:* + version: link:../../integrations/messenger + '@chatbotx.io/integration-moosend': + specifier: workspace:* + version: link:../../integrations/moosend + '@chatbotx.io/integration-sendgrid': + specifier: workspace:* + version: link:../../integrations/sendgrid + '@chatbotx.io/integration-smtp': + specifier: workspace:* + version: link:../../integrations/smtp + '@chatbotx.io/integration-telegram': + specifier: workspace:* + version: link:../../integrations/telegram + '@chatbotx.io/integration-tiktok': + specifier: workspace:* + version: link:../../integrations/tiktok + '@chatbotx.io/integration-webchat': + specifier: workspace:* + version: link:../../integrations/webchat + '@chatbotx.io/integration-whatsapp': + specifier: workspace:* + version: link:../../integrations/whatsapp + '@chatbotx.io/integration-zalo': + specifier: workspace:* + version: link:../../integrations/zalo + '@chatbotx.io/logger': + specifier: workspace:* + version: link:../logger + '@chatbotx.io/sdk': + specifier: workspace:* + version: link:../sdk + '@chatbotx.io/utils': + specifier: workspace:* + version: link:../utils + devDependencies: + '@chatbotx.io/typescript-config': + specifier: workspace:* + version: link:../typescript-config + '@chatbotx.io/vitest-config': + specifier: workspace:* + version: link:../vitest-config + '@types/node': + specifier: 24.x + version: 24.13.1 + typescript: + specifier: ^5 + version: 5.9.3 + vitest: + specifier: ^4.1.0 + version: 4.1.8(@opentelemetry/api@1.9.1)(@types/node@24.13.1)(@vitest/coverage-v8@4.1.8)(jsdom@25.0.1)(msw@2.14.6(@types/node@24.13.1)(typescript@5.9.3))(vite@7.3.5(@types/node@24.13.1)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.23.0)(yaml@2.9.0)) + packages/database: dependencies: '@chatbotx.io/encryption': From eecc9a451c6629161ab076efad77aad8ef2e14a4 Mon Sep 17 00:00:00 2001 From: Real Codesiman Date: Tue, 15 Sep 2026 17:40:36 +0700 Subject: [PATCH 2/2] fix(connections): resolve critical/important findings from PR #1185 review Addresses the code-review findings on the Connection-lifecycle unification (unify channel/app integration connect lifecycle, commit f43710567): Critical: - ConnectSession.create writes targets/claimedTargetIds/resultConnectionIds/ results explicitly (drizzle-kit never inlines a sql template default for jsonb/array columns) - every OAuth connect was NOT NULL-violating on insert. - markOffline(reason) branches on instanceof AuthException: a revoked token still marks unhealthy, but a transient refresh failure now degrades (quota held) instead of releasing the channel's quota slot. - Messenger/Instagram/InstagramFacebook disconnect actions now transition the Connection row (not just Inbox), fixing a quota double-charge on connect -> UI-disconnect -> reconnect. - Telegram plus the 8 api_key marketing providers gained a live fromCredentials validator; api/smtp/webchat/chatbotx are explicitly gated unavailableReason "notImplemented" pending their sourceId identity model instead of silently 500ing. Important: - Telegram's Connection.sourceId derives from the bot token's numeric prefix instead of a colliding "workspace" constant. - Disconnect teardown failures are surfaced onto Connection.lastError; the satellite-row delete and FSM transition share one db.transaction. - Telegram's verify() returns a ConnectionHealth failure instead of throwing. - drip/activeCampaign/getResponse's isRevokedTokenError TODO stubs now classify real 401/403 responses. - ConnectionStateService.transition consumes quota before the status write (never needs a rollback) and throws instead of silently skipping a required quota edge with no resolvable owner. - completeAuthorization's exchangeCode catch now records exchange_failed instead of overloading provider_denied (real denial is filtered upstream by the OAuth callback's own error query-param check). - Messenger's exchangeCode/listCandidates carry the real callbackUrl as redirectUrl instead of an empty string. - /connect is public (the ConnectSession completion page never requires a builder session). Simplifications: - packages/connections/src/service.ts split into internal.ts, lifecycle.ts, credentials.ts, and connect-session-flow.ts (each under the 800-line cap), deduplicating the revive-or-insert-then-transition block into upsertConnectionRow. - Shared isUnauthorizedStatusError SDK helper replaces duplicated 401/403 duck-typing across 7 marketing providers. - Shared verifyMetaToken(label) factory for Messenger/Instagram-Facebook. - .agents/rules/data-access.md documents packages/connections as the registry-aware tier above packages/business. Plus test coverage for every fix above (ordering, auth-safety, scope enforcement on the public API, the inbox-status write guard, registry invariants) and five stale comments and docs corrected. --- .agents/rules/data-access.md | 6 +- .../connections-public-permission.test.ts | 231 +++ .../__tests__/disconnect-meta-actions.test.ts | 47 + .../__tests__/proxy-public-routes.test.ts | 5 + .../__tests__/resolve-provider.test.ts | 86 ++ .../connect/[sessionId]/resolve-message.ts | 16 +- .../connections/lib/resolve-provider.ts | 18 + .../actions/disconnect-instagram.ts | 39 +- .../actions/disconnect-messenger.ts | 39 +- apps/builder/src/lib/public-routes.ts | 5 + docs/developer/workspace-api-tokens.md | 9 +- .../__tests__/connection.test.ts | 57 + .../active-campaign/src/integration.ts | 31 +- .../drip/__tests__/connection.test.ts | 62 + integrations/drip/src/integration.ts | 34 +- .../get-response/__tests__/connection.test.ts | 55 + integrations/get-response/src/integration.ts | 36 +- .../instagram-facebook/src/apis/auth.ts | 35 + .../instagram-facebook/src/integration.ts | 20 +- .../klaviyo/__tests__/connection.test.ts | 51 + integrations/klaviyo/src/integration.ts | 40 +- .../mailchimp/__tests__/connection.test.ts | 65 + integrations/mailchimp/package.json | 8 +- integrations/mailchimp/src/integration.ts | 35 +- integrations/mailchimp/vitest.config.ts | 1 + .../mailer-lite/__tests__/connection.test.ts | 53 + integrations/mailer-lite/src/integration.ts | 42 +- .../__tests__/connection-oauth.test.ts | 2 +- integrations/messenger/src/apis/auth.ts | 35 + integrations/messenger/src/integration.ts | 28 +- .../moosend/__tests__/connection.test.ts | 55 + integrations/moosend/src/integration.ts | 23 +- .../sendgrid/__tests__/connection.test.ts | 60 + integrations/sendgrid/src/integration.ts | 60 +- .../telegram/__tests__/connection.test.ts | 88 ++ integrations/telegram/src/integration.ts | 33 +- .../inbox-status-write-guard.test.ts | 65 + .../business/src/connect-session/service.ts | 10 + .../__tests__/state-service.test.ts | 35 + .../src/connection/credential-providers.ts | 13 +- .../business/src/connection/state-service.ts | 204 +-- .../__tests__/auth-store.test.ts | 58 +- .../src/integration-context/auth-store.ts | 37 +- .../connections/__tests__/registry.test.ts | 42 + .../connections/__tests__/service.test.ts | 75 +- .../connections/src/connect-session-flow.ts | 581 ++++++++ packages/connections/src/credentials.ts | 177 +++ packages/connections/src/internal.ts | 336 +++++ packages/connections/src/lifecycle.ts | 220 +++ packages/connections/src/service.ts | 1248 +---------------- .../__tests__/connection-repository.test.ts | 51 + .../insert-required-columns.test.ts | 22 + .../integration/schema-default-parity.test.ts | 4 + .../database/src/schema/connect-session.ts | 3 +- packages/sdk/src/lib/exception.ts | 15 + packages/sdk/src/lib/integration.ts | 14 +- packages/utils/src/connection.ts | 1 + pnpm-lock.yaml | 6 + 58 files changed, 3189 insertions(+), 1538 deletions(-) create mode 100644 apps/builder/__tests__/connections-public-permission.test.ts create mode 100644 apps/builder/__tests__/resolve-provider.test.ts create mode 100644 integrations/active-campaign/__tests__/connection.test.ts create mode 100644 integrations/drip/__tests__/connection.test.ts create mode 100644 integrations/get-response/__tests__/connection.test.ts create mode 100644 integrations/klaviyo/__tests__/connection.test.ts create mode 100644 integrations/mailchimp/__tests__/connection.test.ts create mode 100644 integrations/mailchimp/vitest.config.ts create mode 100644 integrations/mailer-lite/__tests__/connection.test.ts create mode 100644 integrations/moosend/__tests__/connection.test.ts create mode 100644 integrations/sendgrid/__tests__/connection.test.ts create mode 100644 integrations/telegram/__tests__/connection.test.ts create mode 100644 packages/business/__tests__/inbox-status-write-guard.test.ts create mode 100644 packages/connections/src/connect-session-flow.ts create mode 100644 packages/connections/src/credentials.ts create mode 100644 packages/connections/src/internal.ts create mode 100644 packages/connections/src/lifecycle.ts create mode 100644 packages/database/__tests__/connection-repository.test.ts diff --git a/.agents/rules/data-access.md b/.agents/rules/data-access.md index e8f14c016b..b686955d4e 100644 --- a/.agents/rules/data-access.md +++ b/.agents/rules/data-access.md @@ -3,14 +3,15 @@ name: data-access description: >- Enforces the action/API handler → service → repository → DB chain in ChatbotX. Read before adding or reviewing code in apps/builder, - apps/worker, integrations/*, packages/business, or packages/database that - reads or writes data. + apps/worker, integrations/*, packages/business, packages/connections, or + packages/database that reads or writes data. globs: - apps/builder/** - apps/worker/** - apps/mcp-server/** - integrations/** - packages/business/** + - packages/connections/** - packages/database/** --- @@ -34,6 +35,7 @@ The chain is: **action / API handler → service (`packages/business/`) → repo |-------|---|------| | `packages/database/src/repositories/*` | Yes | Raw where-builders, joins, pagination, shard routing. **Never** cache invalidation, event emission, or validation. | | `packages/business/src/*` | Yes | Validation, orchestration across repositories, cache invalidation, events, audit, quota checks, optional `tx?: DatabaseClient` passthrough. **Never** imports from `apps/` or `integrations/`. | +| `packages/connections/src/*` | Yes | The **registry-aware** orchestration tier for the `Connection` domain, one level above `packages/business` — it composes `CONNECTION_REGISTRY` (each `integrations/`'s SDK adapter) with `packages/business`'s registry-free `connectionStateService`/`connectSessionService`, and owns the transactions that touch both a satellite table row and the `Connection` row in one commit (`upsertConnectionRow`, `disconnect`, `completeReconnect`). It exists as its own tier — not folded into `packages/business` — specifically because `packages/business` must stay registry-free (importable from `markOffline` hooks and webhook handlers without pulling in every provider's module graph); `packages/connections` is the one place allowed to depend on both. Only `connectionService` (from `@chatbotx.io/connections`) is the public surface app/worker code calls — never import `packages/connections/src/internal.ts`'s helpers directly. | | `apps/builder/src/features/*/actions/` | **No** | Parse input → call a service method → map the result/error for the client. | | `apps/builder/src/features/*/queries/` | **No** | See the `.query.ts` contract below. | | `apps/builder/src/features/*/api/` | **No** | Resolve session context into plain params, call the same service method the private path uses. | diff --git a/apps/builder/__tests__/connections-public-permission.test.ts b/apps/builder/__tests__/connections-public-permission.test.ts new file mode 100644 index 0000000000..7c253f4ee0 --- /dev/null +++ b/apps/builder/__tests__/connections-public-permission.test.ts @@ -0,0 +1,231 @@ +// @vitest-environment node + +import { beforeEach, describe, expect, test, vi } from "vitest" + +const { + findWorkspaceByTokenHash, + isWorkspaceScheduledForDeletion, + getAccessState, + isAtLimit, + assertApiNotRateLimited, +} = vi.hoisted(() => ({ + findWorkspaceByTokenHash: vi.fn(), + isWorkspaceScheduledForDeletion: vi.fn().mockReturnValue(false), + getAccessState: vi.fn().mockResolvedValue({ blocked: false }), + isAtLimit: vi.fn().mockResolvedValue(false), + assertApiNotRateLimited: vi.fn().mockResolvedValue(undefined), +})) + +const connectionServiceMocks = vi.hoisted(() => ({ + disconnect: vi.fn(), + refresh: vi.fn(), + verify: vi.fn(), + connectFromCredentials: vi.fn(), + startSession: vi.fn(), + reconnect: vi.fn(), + connectTargets: vi.fn(), +})) + +vi.mock("@chatbotx.io/business", () => ({ + workspaceApiTokenService: { findWorkspaceByTokenHash }, + isWorkspaceScheduledForDeletion, + userQuotaService: { getAccessState }, + quotaEnforcementService: { isAtLimit }, + connectionStateService: { + list: vi.fn(), + getForWorkspace: vi.fn(), + updateDisplayName: vi.fn(), + }, + platformCredentialService: { resolveForOwner: vi.fn() }, +})) + +vi.mock("@chatbotx.io/business/connect-session", () => ({ + connectSessionService: { + findByIdForWorkspace: vi.fn(), + requireByIdForWorkspace: vi.fn(), + submitInput: vi.fn(), + cancel: vi.fn(), + }, +})) + +vi.mock("@chatbotx.io/connections", () => ({ + connectionService: connectionServiceMocks, + CONNECTION_REGISTRY: { + messenger: { provider: { strategy: "oauth_redirect", kind: "channel" } }, + }, +})) + +vi.mock("../src/features/connections/lib/resolve-provider", () => ({ + toConnectionResource: vi.fn((row: { id: string }) => ({ id: row.id })), + listConnectionProviderResources: vi.fn(), + channelForProvider: vi.fn(() => undefined), +})) + +vi.mock("../src/features/connections/lib/connect-session-resource", () => ({ + toConnectSessionResource: vi.fn((row: { id: string }) => ({ id: row.id })), +})) + +vi.mock("../src/features/connections/lib/resolve-connect-credential", () => ({ + resolveOAuthCredential: vi.fn(), +})) + +vi.mock("@/lib/oauth-referer", () => ({ + sanitizeOptionalReturnUrl: vi.fn(async (url?: string) => url), +})) + +vi.mock("@/lib/platform-credential-owner", () => ({ + resolveOwnerForWorkspace: vi.fn(async () => "owner-1"), +})) + +vi.mock("@/lib/workspace/resolve-visible-channels", () => ({ + resolveChannelPolicy: vi.fn(async () => null), +})) + +vi.mock("@/lib/log", () => ({ + logger: { warn: vi.fn(), error: vi.fn() }, +})) + +vi.mock("@/lib/rate-limit/api-rate-limit", () => ({ + assertApiNotRateLimited, +})) + +vi.mock("@/lib/rate-limit/guest-rate-limit", () => ({ + getGuestClientIp: () => "203.0.113.9", +})) + +vi.mock("@/env", () => ({ isCloud: () => true })) + +// `@/orpc` also exports `authorizedAPI`, which pulls in the full better-auth +// stack via `authMiddleware` — irrelevant here and unsafe to initialize in a +// unit test. Same stub as workspace-token-scope-enforcement.test.ts. +vi.mock("@/middlewares/auth", () => ({ + authMiddleware: vi.fn(), +})) + +const { call } = await import("@orpc/server") +const { connectionsPublicRouter } = await import( + "../src/features/connections/api/public" +) + +const TOKEN = "cbx_ws_fixture" + +const invoke = (procedure: unknown, input: unknown = {}) => + call(procedure as Parameters[0], input, { + context: { headers: new Headers({ Authorization: `Bearer ${TOKEN}` }) }, + }) + +beforeEach(() => { + vi.clearAllMocks() + isWorkspaceScheduledForDeletion.mockReturnValue(false) + getAccessState.mockResolvedValue({ blocked: false }) + isAtLimit.mockResolvedValue(false) + assertApiNotRateLimited.mockResolvedValue(undefined) +}) + +describe("real router: connections public API permission enforcement (T5)", () => { + test("a read_only token is denied DELETE /v1/connections/{id} before any service call", async () => { + findWorkspaceByTokenHash.mockResolvedValue({ + workspace: { id: "ws-1", ownerId: "owner-1" }, + apiToken: { + id: "token-1", + permission: "read_only" as const, + scopes: null, + }, + }) + + await expect( + invoke(connectionsPublicRouter.disconnect, { id: "conn-1" }), + ).rejects.toMatchObject({ code: "FORBIDDEN" }) + + expect(connectionServiceMocks.disconnect).not.toHaveBeenCalled() + }) + + test("a read_only token is denied POST /v1/connections before any service call", async () => { + findWorkspaceByTokenHash.mockResolvedValue({ + workspace: { id: "ws-1", ownerId: "owner-1" }, + apiToken: { + id: "token-1", + permission: "read_only" as const, + scopes: null, + }, + }) + + await expect( + invoke(connectionsPublicRouter.create, { + provider: "messenger", + }), + ).rejects.toMatchObject({ code: "FORBIDDEN" }) + + expect(connectionServiceMocks.connectFromCredentials).not.toHaveBeenCalled() + expect(connectionServiceMocks.startSession).not.toHaveBeenCalled() + }) + + test("a read_only token is denied POST /v1/connections/{id}/refresh before any service call", async () => { + findWorkspaceByTokenHash.mockResolvedValue({ + workspace: { id: "ws-1", ownerId: "owner-1" }, + apiToken: { + id: "token-1", + permission: "read_only" as const, + scopes: null, + }, + }) + + await expect( + invoke(connectionsPublicRouter.refresh, { id: "conn-1" }), + ).rejects.toMatchObject({ code: "FORBIDDEN" }) + + expect(connectionServiceMocks.refresh).not.toHaveBeenCalled() + }) + + test("a read_only token is denied POST /v1/connections/{id}/verify before any service call", async () => { + findWorkspaceByTokenHash.mockResolvedValue({ + workspace: { id: "ws-1", ownerId: "owner-1" }, + apiToken: { + id: "token-1", + permission: "read_only" as const, + scopes: null, + }, + }) + + await expect( + invoke(connectionsPublicRouter.verify, { id: "conn-1" }), + ).rejects.toMatchObject({ code: "FORBIDDEN" }) + + expect(connectionServiceMocks.verify).not.toHaveBeenCalled() + }) + + test("a read_only token is denied POST /v1/connections/{id}/reconnect before any service call", async () => { + findWorkspaceByTokenHash.mockResolvedValue({ + workspace: { id: "ws-1", ownerId: "owner-1" }, + apiToken: { + id: "token-1", + permission: "read_only" as const, + scopes: null, + }, + }) + + await expect( + invoke(connectionsPublicRouter.reconnect, { id: "conn-1" }), + ).rejects.toMatchObject({ code: "FORBIDDEN" }) + + expect(connectionServiceMocks.reconnect).not.toHaveBeenCalled() + }) + + test("a contacts-scoped token is denied the real DELETE /v1/connections/{id} route with FORBIDDEN", async () => { + findWorkspaceByTokenHash.mockResolvedValue({ + workspace: { id: "ws-1", ownerId: "owner-1" }, + apiToken: { + id: "token-1", + permission: "full" as const, + scopes: ["contacts"], + }, + }) + + await expect( + invoke(connectionsPublicRouter.disconnect, { id: "conn-1" }), + ).rejects.toMatchObject({ + code: "FORBIDDEN", + message: "Token is not authorized for the 'connections' scope", + }) + }) +}) diff --git a/apps/builder/__tests__/disconnect-meta-actions.test.ts b/apps/builder/__tests__/disconnect-meta-actions.test.ts index f9d35a2903..d6f31cf293 100644 --- a/apps/builder/__tests__/disconnect-meta-actions.test.ts +++ b/apps/builder/__tests__/disconnect-meta-actions.test.ts @@ -21,6 +21,8 @@ const mocks = vi.hoisted(() => { callback(tx), ), coexistTearDownForIntegration: vi.fn().mockResolvedValue(undefined), + connectionFindByInboxId: vi.fn().mockResolvedValue(undefined), + connectionTransition: vi.fn().mockResolvedValue(undefined), findOrFail: vi.fn(), inboxDisconnect: vi.fn().mockResolvedValue(undefined), instagramExists: vi.fn().mockResolvedValue(false), @@ -50,6 +52,10 @@ const mocks = vi.hoisted(() => { } }) +vi.mock("@chatbotx.io/business/connection", () => ({ + connectionStateService: { transition: mocks.connectionTransition }, +})) + vi.mock("@chatbotx.io/business/audit", () => ({ auditService: { record: mocks.auditRecord }, })) @@ -81,6 +87,7 @@ vi.mock("@chatbotx.io/database/client", () => ({ })) vi.mock("@chatbotx.io/database/repositories", () => ({ + connectionRepository: { findByInboxId: mocks.connectionFindByInboxId }, metaCapiEventRepository: { deleteByIntegration: mocks.metaCapiDeleteByIntegration, }, @@ -189,6 +196,7 @@ describe("Meta disconnect actions", () => { mocks.instagramDisconnect.mockResolvedValue(undefined) mocks.instagramFacebookDisconnect.mockResolvedValue(undefined) mocks.subscribePageToAppWebhook.mockResolvedValue(undefined) + mocks.connectionFindByInboxId.mockResolvedValue(undefined) mocks.workspaceFindById.mockResolvedValue({ id: "workspace-1", ownerId: "owner-1", @@ -329,4 +337,43 @@ describe("Meta disconnect actions", () => { detail: "disconnected the Messenger channel (#messenger-1)", }) }) + + test("messenger disconnect transitions the Connection row instead of writing Inbox directly when one exists", async () => { + mocks.findOrFail.mockResolvedValueOnce(messengerRow) + mocks.connectionFindByInboxId.mockResolvedValueOnce({ + id: "conn-1", + workspaceId: "workspace-1", + }) + + await disconnectMessenger({ workspaceId: "workspace-1", id: "messenger-1" }) + + expect(mocks.connectionTransition).toHaveBeenCalledWith({ + connectionId: "conn-1", + event: "user.disconnect", + ownerId: "owner-1", + tx: mocks.tx, + }) + expect(mocks.inboxDisconnect).not.toHaveBeenCalled() + }) + + test("instagram disconnect transitions the Connection row instead of writing Inbox directly when one exists", async () => { + mocks.findOrFail.mockResolvedValueOnce(instagramFacebookRow) + mocks.connectionFindByInboxId.mockResolvedValueOnce({ + id: "conn-2", + workspaceId: "workspace-1", + }) + + await disconnectInstagram({ + workspaceId: "workspace-1", + integrationInstagramId: "instagram-1", + }) + + expect(mocks.connectionTransition).toHaveBeenCalledWith({ + connectionId: "conn-2", + event: "user.disconnect", + ownerId: "owner-1", + tx: mocks.tx, + }) + expect(mocks.inboxDisconnect).not.toHaveBeenCalled() + }) }) diff --git a/apps/builder/__tests__/proxy-public-routes.test.ts b/apps/builder/__tests__/proxy-public-routes.test.ts index 6980a3310e..ca460a70ca 100644 --- a/apps/builder/__tests__/proxy-public-routes.test.ts +++ b/apps/builder/__tests__/proxy-public-routes.test.ts @@ -31,4 +31,9 @@ describe("isPublicRoute", () => { expect(isPublicRoute("/apikeys")).toBe(false) expect(isPublicRoute("/authorized-apps")).toBe(false) }) + + test("/connect is public so an unauthenticated ConnectSession completion works (T8)", () => { + expect(isPublicRoute("/connect")).toBe(true) + expect(isPublicRoute("/connect/sess-abc123")).toBe(true) + }) }) diff --git a/apps/builder/__tests__/resolve-provider.test.ts b/apps/builder/__tests__/resolve-provider.test.ts new file mode 100644 index 0000000000..49535b040d --- /dev/null +++ b/apps/builder/__tests__/resolve-provider.test.ts @@ -0,0 +1,86 @@ +// @vitest-environment node + +import { describe, expect, test, vi } from "vitest" + +vi.mock("@chatbotx.io/connections", () => ({ + CONNECTION_REGISTRY: { + messenger: { + provider: { + strategy: "oauth_redirect", + multiAccount: true, + verify: vi.fn(), + }, + integration: { refreshAuth: vi.fn() }, + }, + }, +})) + +const { toConnectionResource } = await import( + "../src/features/connections/lib/resolve-provider" +) + +describe("toConnectionResource", () => { + test("never emits an auth-like key even when the row carries one (T5)", () => { + const row = { + id: "conn-1", + workspaceId: "ws-1", + kind: "channel", + provider: "messenger", + channel: "messenger", + status: "connected", + statusReason: null, + sourceId: "page-1", + displayName: "My Page", + inboxId: "inbox-1", + integrationId: null, + authExpiresAt: null, + lastError: null, + connectedAt: new Date("2026-01-01T00:00:00.000Z"), + disconnectedAt: null, + createdAt: new Date("2026-01-01T00:00:00.000Z"), + updatedAt: new Date("2026-01-01T00:00:00.000Z"), + createdBy: null, + // Simulates a future column addition or a caller that forwards the + // full row without stripping the credential first — the DTO + // projection must still exclude it, not rely on the caller. + auth: { accessToken: "secret-token" }, + encryptedAuth: { iv: "iv", content: "encrypted", tag: "tag" }, + } as any + + const resource = toConnectionResource(row) + + expect(resource).not.toHaveProperty("auth") + expect(resource).not.toHaveProperty("encryptedAuth") + expect(JSON.stringify(resource)).not.toContain("secret-token") + }) + + test("resolves capabilities from the registry adapter", () => { + const row = { + id: "conn-1", + kind: "channel", + provider: "messenger", + channel: "messenger", + status: "connected", + statusReason: null, + sourceId: "page-1", + displayName: "My Page", + inboxId: "inbox-1", + integrationId: null, + authExpiresAt: null, + lastError: null, + connectedAt: null, + disconnectedAt: null, + createdAt: new Date("2026-01-01T00:00:00.000Z"), + updatedAt: new Date("2026-01-01T00:00:00.000Z"), + } as any + + const resource = toConnectionResource(row) + + expect(resource.strategy).toBe("oauth_redirect") + expect(resource.capabilities).toEqual({ + refreshable: true, + verifiable: true, + multiAccount: true, + }) + }) +}) diff --git a/apps/builder/src/app/connect/[sessionId]/resolve-message.ts b/apps/builder/src/app/connect/[sessionId]/resolve-message.ts index d799be49e9..6b87613572 100644 --- a/apps/builder/src/app/connect/[sessionId]/resolve-message.ts +++ b/apps/builder/src/app/connect/[sessionId]/resolve-message.ts @@ -3,12 +3,16 @@ import type { ConnectSessionStatus } from "@chatbotx.io/database/partials" /** * Which message block `/connect/{sessionId}` shows for a given * `ConnectSession.status` (or `null` when the session id resolved to - * nothing at all). `"pending"`/`"authorized"`/`"awaiting_selection"` are - * ALL still-in-flight states — including `"authorized"`, the brief window - * between a successful OAuth exchange and candidate listing — so they all - * map to `"processing"` (auto-refreshing), never `"failed"`. Every other - * value (including a genuinely unknown future status) falls back to - * `"failed"` rather than silently misreporting progress as success. + * nothing at all). `"pending"`/`"awaiting_selection"` are still-in-flight + * states, mapped to `"processing"` (auto-refreshing), never `"failed"`. + * `"authorized"` is a declared `ConnectSessionStatus` value with no writer + * today — `attachAuthorization` (`packages/business/src/connect-session/service.ts`) + * goes straight from `pending` to `awaiting_selection`, skipping it — kept + * here (mapped the same as the other in-flight states) only so a future + * intermediate step that DOES write it doesn't silently fall through to + * `"failed"`. Every other value (including a genuinely unknown future + * status) falls back to `"failed"` rather than silently misreporting + * progress as success. * * Extracted as a pure function so this mapping — the one behavior in this * page with real branching to get wrong — is unit-testable without diff --git a/apps/builder/src/features/connections/lib/resolve-provider.ts b/apps/builder/src/features/connections/lib/resolve-provider.ts index e96055698b..925054f9e6 100644 --- a/apps/builder/src/features/connections/lib/resolve-provider.ts +++ b/apps/builder/src/features/connections/lib/resolve-provider.ts @@ -153,6 +153,8 @@ export const channelForProvider = ( : (provider as ChannelType) } +const CREDENTIAL_STRATEGIES = new Set(["token", "api_key", "self_serve"]) + const resolveUnavailableReason = async (input: { provider: IntegrationType policy: Awaited> @@ -164,6 +166,22 @@ const resolveUnavailableReason = async (input: { return "notImplemented" } + // A credential-strategy provider with no live `fromCredentials` validator + // cannot complete `POST /v1/connections` — advertising it as `available` + // would 500 on `connectionWrongStrategyException`. Webchat/SMTP/the API + // channel are `self_serve` with no external account to validate against + // AND no stable per-instance identity to derive `Connection.sourceId` + // from (`describe()` only receives `auth`, which for these providers is + // minted before any satellite row exists) — deferred to a follow-up that + // resolves that identity gap rather than shipping a second "workspace"- + // literal `sourceId` collision class. + if ( + CREDENTIAL_STRATEGIES.has(adapter.provider.strategy) && + !adapter.provider.fromCredentials + ) { + return "notImplemented" + } + if (!(adapter.provider.multiAccount || !input.alreadyConnected)) { return "alreadyConnected" } diff --git a/apps/builder/src/features/integration-instagram/actions/disconnect-instagram.ts b/apps/builder/src/features/integration-instagram/actions/disconnect-instagram.ts index 7cd4dc8ac0..c011306027 100644 --- a/apps/builder/src/features/integration-instagram/actions/disconnect-instagram.ts +++ b/apps/builder/src/features/integration-instagram/actions/disconnect-instagram.ts @@ -6,8 +6,12 @@ import { workspaceService, } from "@chatbotx.io/business" import { auditService } from "@chatbotx.io/business/audit" +import { connectionStateService } from "@chatbotx.io/business/connection" import { db } from "@chatbotx.io/database/client" -import { metaCapiEventRepository } from "@chatbotx.io/database/repositories" +import { + connectionRepository, + metaCapiEventRepository, +} from "@chatbotx.io/database/repositories" import { type InstagramAuthValue, isRevokedTokenError, @@ -96,13 +100,34 @@ export const disconnectInstagram = async (ctx: { tx, }) - await inboxService.disconnect({ - inboxId: integrationInstagram.inboxId, - ownerId: workspace.ownerId, - workspaceId: ctx.workspaceId, - reason: "manual", + // Writing through `connectionStateService` (not `inboxService.disconnect`) + // keeps the `Connection` row and `Inbox.status` in lockstep — Messenger/ + // Instagram/InstagramFacebook connect already write `Connection` via + // `ConnectionService.connectTargets`, so leaving this on the legacy + // direct write would silently drift the two tables and let a + // reconnect skip quota consumption (`connect.completed` from an already- + // `connected` row is a same-state no-op). Falls back to the legacy path + // only for a pre-backfill row with no `Connection` counterpart yet. + const connection = await connectionRepository.findByInboxId( + { inboxId: integrationInstagram.inboxId }, tx, - }) + ) + if (connection) { + await connectionStateService.transition({ + connectionId: connection.id, + event: "user.disconnect", + ownerId: workspace.ownerId, + tx, + }) + } else { + await inboxService.disconnect({ + inboxId: integrationInstagram.inboxId, + ownerId: workspace.ownerId, + workspaceId: ctx.workspaceId, + reason: "manual", + tx, + }) + } }) await auditService.record({ diff --git a/apps/builder/src/features/integration-messenger/actions/disconnect-messenger.ts b/apps/builder/src/features/integration-messenger/actions/disconnect-messenger.ts index 1342c7fe6e..ec7f4a5a33 100644 --- a/apps/builder/src/features/integration-messenger/actions/disconnect-messenger.ts +++ b/apps/builder/src/features/integration-messenger/actions/disconnect-messenger.ts @@ -6,8 +6,12 @@ import { workspaceService, } from "@chatbotx.io/business" import { auditService } from "@chatbotx.io/business/audit" +import { connectionStateService } from "@chatbotx.io/business/connection" import { db } from "@chatbotx.io/database/client" -import { metaCapiEventRepository } from "@chatbotx.io/database/repositories" +import { + connectionRepository, + metaCapiEventRepository, +} from "@chatbotx.io/database/repositories" import { isDisconnectSafeError, type MessengerAuthValue, @@ -102,13 +106,34 @@ export const disconnectMessenger = async (ctx: { tx, }) - await inboxService.disconnect({ - inboxId: integrationMessenger.inboxId, - ownerId: workspace.ownerId, - workspaceId: ctx.workspaceId, - reason: "manual", + // Writing through `connectionStateService` (not `inboxService.disconnect`) + // keeps the `Connection` row and `Inbox.status` in lockstep — Messenger/ + // Instagram/InstagramFacebook connect already write `Connection` via + // `ConnectionService.connectTargets`, so leaving this on the legacy + // direct write would silently drift the two tables and let a + // reconnect skip quota consumption (`connect.completed` from an already- + // `connected` row is a same-state no-op). Falls back to the legacy path + // only for a pre-backfill row with no `Connection` counterpart yet. + const connection = await connectionRepository.findByInboxId( + { inboxId: integrationMessenger.inboxId }, tx, - }) + ) + if (connection) { + await connectionStateService.transition({ + connectionId: connection.id, + event: "user.disconnect", + ownerId: workspace.ownerId, + tx, + }) + } else { + await inboxService.disconnect({ + inboxId: integrationMessenger.inboxId, + ownerId: workspace.ownerId, + workspaceId: ctx.workspaceId, + reason: "manual", + tx, + }) + } }) await auditService.record({ diff --git a/apps/builder/src/lib/public-routes.ts b/apps/builder/src/lib/public-routes.ts index 248efbf37f..e1bb968ed0 100644 --- a/apps/builder/src/lib/public-routes.ts +++ b/apps/builder/src/lib/public-routes.ts @@ -27,6 +27,11 @@ export const PUBLIC_ROUTES = [ "/portal/redeem", "/webchat", "/t/", + // The neutral `ConnectSession` completion page — the person finishing an + // API/MCP-started OAuth connect is never necessarily a builder user or a + // member of the workspace that started it. `sessionId` itself is the + // capability token (see `app/connect/[sessionId]/page.tsx`'s doc comment). + "/connect", ] /** diff --git a/docs/developer/workspace-api-tokens.md b/docs/developer/workspace-api-tokens.md index 7ba13397e6..1bb6bb34d4 100644 --- a/docs/developer/workspace-api-tokens.md +++ b/docs/developer/workspace-api-tokens.md @@ -356,8 +356,13 @@ publish these. | `GET/PUT/DELETE /v1/integrations/ai/{provider}` | Get/upsert/disconnect an AI-provider API key (`claude`, `deepseek`, `gemini`, `openai`) via `integrationService`. `PUT` live-validates the key with `verifyAiProviderApiKey` before persisting. | | `GET/POST /v1/webhooks`, `DELETE /v1/webhooks/{id}` | Full CRUD via `webhookService`. | | `GET/POST /v1/external-webhooks`, `DELETE /v1/external-webhooks/{id}` | Full CRUD via `externalWebhookService`, scoped to platforms like Make; `POST` is idempotent on `(event, url)`. | -| `GET /v1/connections`, `GET /v1/connections/{id}` | Read-only, `ORDER BY kind, provider, displayName, id`, via `connectionStateService`. The unified successor to the four rows above and `GET /v1/integrations` — not a replacement yet (nothing is marked `deprecated: true` until its Phase 2/3 write-path equivalent ships), but the shared read path both the public and private (`GET /workspaces/{workspaceId}/connections`) routes call. `capabilities` (`refreshable`/`verifiable`/`multiAccount`) is joined from `CONNECTION_REGISTRY` at response time — never stored on the row. `auth` is never returned. | -| `GET /v1/connection-providers` | Read-only connect catalog — every `IntegrationType`'s `strategy`, `configFields` (labels resolved from the request locale, falling back to the raw field name for a key with no translation yet), and `available`/`unavailableReason` (`notImplemented` \| `hiddenForTenant` \| `alreadyConnected` \| `credentialMissing`) resolved against this workspace via `resolveChannelPolicy` + `platformCredentialService.resolveForOwner`. No `POST /v1/connections` yet — Phase 3. | +| `GET /v1/connections`, `GET /v1/connections/{id}` | Read-only, `ORDER BY kind, provider, displayName, id`, via `connectionStateService`. The unified successor to the four rows above and `GET /v1/integrations` — not a replacement yet (nothing is marked `deprecated: true` until its write-path equivalent for every provider ships), but the shared read path both the public and private (`GET /workspaces/{workspaceId}/connections`) routes call. `capabilities` (`refreshable`/`verifiable`/`multiAccount`) is joined from `CONNECTION_REGISTRY` at response time — never stored on the row. `auth` is never returned. | +| `POST /v1/connections` | Connects a channel or integration. Credential-strategy providers (`token`/`api_key`/`self_serve` with `fromCredentials` defined) connect immediately and return `connection`; OAuth providers return a `session` to poll. Only a subset of providers ship `fromCredentials` today — see `GET /v1/connection-providers`'s `unavailableReason: "notImplemented"` for the rest. | +| `PATCH /v1/connections/{id}` | Renames the connection's `displayName` only. | +| `DELETE /v1/connections/{id}` | Disconnects — best-effort provider-side teardown, then the local FSM transition. Denied for a `read_only` token. | +| `POST /v1/connections/{id}/refresh`, `POST /v1/connections/{id}/verify`, `POST /v1/connections/{id}/reconnect` | Force a refresh, run a live health check, or start a re-authorization OAuth session for an existing connection. | +| `GET /v1/connect-sessions/{id}`, `POST /v1/connect-sessions/{id}/targets`, `POST /v1/connect-sessions/{id}/input`, `DELETE /v1/connect-sessions/{id}` | Poll/complete/cancel a multi-step OAuth connect session started by `POST /v1/connections`/`.../reconnect`. | +| `GET /v1/connection-providers` | Read-only connect catalog — every `IntegrationType`'s `strategy`, `configFields` (labels resolved from the request locale, falling back to the raw field name for a key with no translation yet), and `available`/`unavailableReason` (`notImplemented` \| `hiddenForTenant` \| `alreadyConnected` \| `credentialMissing`) resolved against this workspace via `resolveChannelPolicy` + `platformCredentialService.resolveForOwner`. | Two invariants specific to this scope: diff --git a/integrations/active-campaign/__tests__/connection.test.ts b/integrations/active-campaign/__tests__/connection.test.ts new file mode 100644 index 0000000000..5c0d2a4096 --- /dev/null +++ b/integrations/active-campaign/__tests__/connection.test.ts @@ -0,0 +1,57 @@ +import { afterEach, describe, expect, test, vi } from "vitest" +import { integration } from "../src/integration" + +const jsonResponse = (body: unknown, status = 200) => + new Response(JSON.stringify(body), { + status, + headers: { "Content-Type": "application/json" }, + }) + +const connection = integration.connection +if (!connection?.fromCredentials) { + throw new Error( + "activeCampaign integration has no connection.fromCredentials", + ) +} + +afterEach(() => { + vi.unstubAllGlobals() +}) + +describe("activeCampaign connection.fromCredentials", () => { + test("live-validates the credential and returns a custom auth value", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async () => jsonResponse({ accounts: [] })), + ) + + const auth = await connection.fromCredentials?.({ + apiUrl: "https://example.api-us1.com/api/3/", + apiKey: " key ", + }) + + expect(auth).toMatchObject({ authType: "custom" }) + }) + + test("rejects an invalid credential by rethrowing the accounts failure", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async () => jsonResponse({}, 401)), + ) + + await expect( + connection.fromCredentials?.({ + apiUrl: "https://example.api-us1.com/api/3/", + apiKey: "bad-key", + }), + ).rejects.toBeTruthy() + }) +}) + +describe("activeCampaign connection.isRevokedTokenError", () => { + test("is a real predicate, not a stub", () => { + expect(connection.isRevokedTokenError({ statusCode: 401 })).toBe(true) + expect(connection.isRevokedTokenError({ statusCode: 403 })).toBe(true) + expect(connection.isRevokedTokenError({ statusCode: 500 })).toBe(false) + }) +}) diff --git a/integrations/active-campaign/src/integration.ts b/integrations/active-campaign/src/integration.ts index 40dd6bf00d..67a78c4153 100644 --- a/integrations/active-campaign/src/integration.ts +++ b/integrations/active-campaign/src/integration.ts @@ -1,6 +1,7 @@ import { Integration, type IntegrationDefinition, + isUnauthorizedStatusError, SdkException, } from "@chatbotx.io/sdk" import { activeCampaignRequest } from "./client" @@ -22,6 +23,7 @@ import { type ActiveCampaignAuthValue, type ActiveCampaignConfig, type ActiveCampaignContactAutomationPayload, + type ActiveCampaignCredentialValue, activeCampaignAccountsResponseSchema, activeCampaignAutomationsResponseSchema, activeCampaignContactAutomationPayloadSchema, @@ -67,6 +69,19 @@ const contactAutomationExists = async ( ) } +/** Shared by `connection.fromCredentials` (live-validate + return `AuthValue`) and the legacy `validateCredentials` action. */ +const buildActiveCampaignAuth = async ( + config: ActiveCampaignCredentialValue, +): Promise => { + const credential = activeCampaignCredentialSchema.parse(config) + await activeCampaignRequest( + credential, + activeCampaignAccountsPath(), + activeCampaignAccountsResponseSchema, + ) + return createActiveCampaignAuth(credential) +} + const config: IntegrationDefinition< ActiveCampaignConfig, ActiveCampaignAuthValue, @@ -96,6 +111,7 @@ const config: IntegrationDefinition< sourceId: "workspace", displayName: "ActiveCampaign", }), + fromCredentials: buildActiveCampaignAuth, verify: async ({ auth }) => { try { await activeCampaignRequest( @@ -107,7 +123,7 @@ const config: IntegrationDefinition< } catch (error) { return { ok: false, - revoked: false, + revoked: isUnauthorizedStatusError(error), error: error instanceof Error ? error.message @@ -115,19 +131,10 @@ const config: IntegrationDefinition< } } }, - // TODO(connection-phase2): refine once ActiveCampaign revoked-token error shape is confirmed. - isRevokedTokenError: () => false, + isRevokedTokenError: isUnauthorizedStatusError, }, actions: { - validateCredentials: async ({ props }) => { - const credential = activeCampaignCredentialSchema.parse(props) - await activeCampaignRequest( - credential, - activeCampaignAccountsPath(), - activeCampaignAccountsResponseSchema, - ) - return createActiveCampaignAuth(credential) - }, + validateCredentials: async ({ props }) => buildActiveCampaignAuth(props), listLists: async ({ ctx }) => { const response = await activeCampaignRequest( ctx.auth, diff --git a/integrations/drip/__tests__/connection.test.ts b/integrations/drip/__tests__/connection.test.ts new file mode 100644 index 0000000000..6c11ce7620 --- /dev/null +++ b/integrations/drip/__tests__/connection.test.ts @@ -0,0 +1,62 @@ +import { afterEach, describe, expect, test, vi } from "vitest" +import { integration } from "../src/integration" + +const jsonResponse = (body: unknown, status = 200) => + new Response(JSON.stringify(body), { + status, + headers: { "Content-Type": "application/json" }, + }) + +const connection = integration.connection +if (!connection?.fromCredentials) { + throw new Error("drip integration has no connection.fromCredentials") +} + +afterEach(() => { + vi.unstubAllGlobals() +}) + +describe("drip connection.fromCredentials", () => { + test("live-validates the api token and returns a custom auth value", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async () => + jsonResponse({ accounts: [{ id: "123", name: "Main" }] }), + ), + ) + + const auth = await connection.fromCredentials?.({ apiToken: " token " }) + + expect(auth).toMatchObject({ authType: "custom", apiToken: "token" }) + }) + + test("rejects a token with no accessible account", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async () => jsonResponse({ accounts: [] })), + ) + + await expect( + connection.fromCredentials?.({ apiToken: "token" }), + ).rejects.toBeTruthy() + }) + + test("rejects an invalid token by rethrowing the accounts failure", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async () => jsonResponse({}, 401)), + ) + + await expect( + connection.fromCredentials?.({ apiToken: "bad-token" }), + ).rejects.toBeTruthy() + }) +}) + +describe("drip connection.isRevokedTokenError", () => { + test("is a real predicate, not a stub", () => { + expect(connection.isRevokedTokenError({ statusCode: 401 })).toBe(true) + expect(connection.isRevokedTokenError({ statusCode: 403 })).toBe(true) + expect(connection.isRevokedTokenError({ statusCode: 500 })).toBe(false) + }) +}) diff --git a/integrations/drip/src/integration.ts b/integrations/drip/src/integration.ts index caa8147046..1f91075123 100644 --- a/integrations/drip/src/integration.ts +++ b/integrations/drip/src/integration.ts @@ -1,6 +1,7 @@ import { Integration, type IntegrationDefinition, + isUnauthorizedStatusError, SdkException, } from "@chatbotx.io/sdk" import { dripRequest } from "./client" @@ -23,6 +24,20 @@ import { dripTagsResponseSchema, } from "./schemas" +/** Shared by `connection.fromCredentials` (live-validate + return `AuthValue`) and the legacy `validateCredentials` action. */ +const buildDripAuth = async (apiToken: string): Promise => { + const auth = createDripAuth(apiToken) + const response = await dripRequest( + auth, + DRIP_ACCOUNTS_PATH, + dripAccountsResponseSchema, + ) + if (response.accounts.length === 0) { + throw new DripNoAccountError() + } + return auth +} + const config: IntegrationDefinition = { name: "drip", connection: { @@ -42,6 +57,8 @@ const config: IntegrationDefinition = { sourceId: "workspace", displayName: "Drip", }), + fromCredentials: (config: { apiToken: string }) => + buildDripAuth(config.apiToken), verify: async ({ auth }) => { try { const response = await dripRequest( @@ -56,7 +73,7 @@ const config: IntegrationDefinition = { } catch (error) { return { ok: false, - revoked: false, + revoked: isUnauthorizedStatusError(error), error: error instanceof Error ? error.message @@ -64,21 +81,10 @@ const config: IntegrationDefinition = { } } }, - // TODO(connection-phase2): refine once Drip revoked-token error shape is confirmed. - isRevokedTokenError: () => false, + isRevokedTokenError: isUnauthorizedStatusError, }, actions: { - validateCredentials: async ({ props }) => { - const response = await dripRequest( - props, - DRIP_ACCOUNTS_PATH, - dripAccountsResponseSchema, - ) - if (response.accounts.length === 0) { - throw new DripNoAccountError() - } - return createDripAuth(props.apiToken) - }, + validateCredentials: async ({ props }) => buildDripAuth(props.apiToken), listAccounts: async ({ ctx }) => { const response = await dripRequest( ctx.auth, diff --git a/integrations/get-response/__tests__/connection.test.ts b/integrations/get-response/__tests__/connection.test.ts new file mode 100644 index 0000000000..785c737cf2 --- /dev/null +++ b/integrations/get-response/__tests__/connection.test.ts @@ -0,0 +1,55 @@ +import { afterEach, describe, expect, test, vi } from "vitest" +import { integration } from "../src/integration" + +const jsonResponse = (body: unknown, status = 200) => + new Response(JSON.stringify(body), { + status, + headers: { "Content-Type": "application/json" }, + }) + +const connection = integration.connection +if (!connection?.fromCredentials) { + throw new Error("getResponse integration has no connection.fromCredentials") +} + +afterEach(() => { + vi.unstubAllGlobals() +}) + +describe("getResponse connection.fromCredentials", () => { + test("live-validates the api key and returns a custom auth value", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async () => + jsonResponse({ + accountId: "account-1", + login: "workspace", + email: "owner@example.com", + }), + ), + ) + + const auth = await connection.fromCredentials?.({ apiKey: " key " }) + + expect(auth).toMatchObject({ authType: "custom" }) + }) + + test("rejects an invalid api key by rethrowing the accounts failure", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async () => jsonResponse({}, 401)), + ) + + await expect( + connection.fromCredentials?.({ apiKey: "bad-key" }), + ).rejects.toBeTruthy() + }) +}) + +describe("getResponse connection.isRevokedTokenError", () => { + test("is a real predicate, not a stub", () => { + expect(connection.isRevokedTokenError({ statusCode: 401 })).toBe(true) + expect(connection.isRevokedTokenError({ statusCode: 403 })).toBe(true) + expect(connection.isRevokedTokenError({ statusCode: 500 })).toBe(false) + }) +}) diff --git a/integrations/get-response/src/integration.ts b/integrations/get-response/src/integration.ts index e41de9bbaf..a4dc6e117f 100644 --- a/integrations/get-response/src/integration.ts +++ b/integrations/get-response/src/integration.ts @@ -1,6 +1,7 @@ import { Integration, type IntegrationDefinition, + isUnauthorizedStatusError, SdkException, } from "@chatbotx.io/sdk" import { getResponseRequest } from "./client" @@ -40,6 +41,21 @@ const mapPageMeta = (props: { total: props.total, }) +/** Shared by `connection.fromCredentials` (live-validate + return `AuthValue`) and the legacy `validateCredentials` action. */ +const buildGetResponseAuth = async ( + apiKey: string, +): Promise => { + const auth = createGetResponseAuth(apiKey) + await getResponseRequest( + auth, + GET_RESPONSE_ACCOUNTS_PATH, + getResponseAccountsResponseSchema, + undefined, + [200], + ) + return auth +} + const config: IntegrationDefinition< GetResponseConfig, GetResponseAuthValue, @@ -63,6 +79,8 @@ const config: IntegrationDefinition< sourceId: "workspace", displayName: "GetResponse", }), + fromCredentials: (config: { apiKey: string }) => + buildGetResponseAuth(config.apiKey), verify: async ({ auth }) => { try { await getResponseRequest( @@ -76,7 +94,7 @@ const config: IntegrationDefinition< } catch (error) { return { ok: false, - revoked: false, + revoked: isUnauthorizedStatusError(error), error: error instanceof Error ? error.message @@ -84,21 +102,11 @@ const config: IntegrationDefinition< } } }, - // TODO(connection-phase2): refine once GetResponse revoked-token error shape is confirmed. - isRevokedTokenError: () => false, + isRevokedTokenError: isUnauthorizedStatusError, }, actions: { - validateCredentials: async ({ props }) => { - const auth = createGetResponseAuth(props.apiKey) - await getResponseRequest( - auth, - GET_RESPONSE_ACCOUNTS_PATH, - getResponseAccountsResponseSchema, - undefined, - [200], - ) - return auth - }, + validateCredentials: async ({ props }) => + buildGetResponseAuth(props.apiKey), listCampaigns: async ({ ctx, props }) => { const response = await getResponseRequest( ctx.auth, diff --git a/integrations/instagram-facebook/src/apis/auth.ts b/integrations/instagram-facebook/src/apis/auth.ts index e89a1f3075..bbeb9f1c22 100644 --- a/integrations/instagram-facebook/src/apis/auth.ts +++ b/integrations/instagram-facebook/src/apis/auth.ts @@ -103,6 +103,41 @@ export function debugToken({ }) } +/** + * Shared `ConnectionProvider.verify` body for a Meta-graph token: inspects + * it via `debugToken` and maps the result to a `ConnectionHealth`. `label` + * only varies the error string (`"${label} access token is invalid"`) — + * Instagram-via-Facebook and Messenger run byte-identical logic otherwise. + */ +export const verifyMetaToken = + (label: string) => + async ({ + auth, + }: { + auth: { + clientId: string + clientSecret: string + tokens: { accessToken: string; expiresAt?: string } + metadata: { version: string } + } + }) => { + const token = await debugToken({ + inputToken: auth.tokens.accessToken, + appAccessToken: toAppAccessToken(auth), + version: auth.metadata.version, + }) + + if (token.is_valid !== true) { + return { + ok: false as const, + revoked: true, + error: `${label} access token is invalid`, + } + } + + return { ok: true as const, authExpiresAt: auth.tokens.expiresAt } + } + export function hasInstagramManageEventsScope( scopes: string[] | undefined, ): boolean { diff --git a/integrations/instagram-facebook/src/integration.ts b/integrations/instagram-facebook/src/integration.ts index 836940cf93..17b60a135a 100644 --- a/integrations/instagram-facebook/src/integration.ts +++ b/integrations/instagram-facebook/src/integration.ts @@ -5,10 +5,10 @@ import { type IntegrationDefinition, } from "@chatbotx.io/sdk" import { - debugToken, exchangeCodeForToken, getUserInstagramAccounts, toAppAccessToken, + verifyMetaToken, } from "./apis/auth" import { exchangeLongLivedToken, @@ -116,23 +116,7 @@ const config: IntegrationDefinition< sourceId: auth.metadata.igId, displayName: auth.metadata.igName, }), - verify: async ({ auth }) => { - const token = await debugToken({ - inputToken: auth.tokens.accessToken, - appAccessToken: toAppAccessToken(auth), - version: auth.metadata.version, - }) - - if (token.is_valid !== true) { - return { - ok: false, - revoked: true, - error: "Instagram access token is invalid", - } - } - - return { ok: true, authExpiresAt: auth.tokens.expiresAt } - }, + verify: verifyMetaToken("Instagram"), isRevokedTokenError, webhook: { subscribe: ({ auth }) => diff --git a/integrations/klaviyo/__tests__/connection.test.ts b/integrations/klaviyo/__tests__/connection.test.ts new file mode 100644 index 0000000000..1a4fe69979 --- /dev/null +++ b/integrations/klaviyo/__tests__/connection.test.ts @@ -0,0 +1,51 @@ +import { afterEach, describe, expect, test, vi } from "vitest" +import { integration } from "../src/integration" + +const jsonResponse = (body: unknown, status = 200) => + new Response(JSON.stringify(body), { + status, + headers: { "Content-Type": "application/json" }, + }) + +const connection = integration.connection +if (!connection?.fromCredentials) { + throw new Error("klaviyo integration has no connection.fromCredentials") +} + +afterEach(() => { + vi.unstubAllGlobals() +}) + +describe("klaviyo connection.fromCredentials", () => { + test("live-validates the api key and returns a custom auth value", async () => { + const fetchMock = vi.fn(async () => + jsonResponse({ data: [], links: { next: null } }), + ) + vi.stubGlobal("fetch", fetchMock) + + const auth = await connection.fromCredentials?.({ apiKey: " key " }) + + expect(auth).toMatchObject({ authType: "custom", apiKey: "key" }) + expect(fetchMock).toHaveBeenCalledTimes(1) + }) + + test("rejects an invalid api key by rethrowing the list-page failure", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async () => jsonResponse({ errors: [] }, 401)), + ) + + await expect( + connection.fromCredentials?.({ apiKey: "bad-key" }), + ).rejects.toBeTruthy() + }) +}) + +describe("klaviyo connection.isRevokedTokenError", () => { + test("is a real predicate, not a stub", () => { + expect(connection.isRevokedTokenError({ statusCode: 401 })).toBe(true) + expect(connection.isRevokedTokenError({ statusCode: 403 })).toBe(true) + expect(connection.isRevokedTokenError({ statusCode: 500 })).toBe(false) + expect(connection.isRevokedTokenError(new Error("boom"))).toBe(false) + }) +}) diff --git a/integrations/klaviyo/src/integration.ts b/integrations/klaviyo/src/integration.ts index 655f4b206c..bf288bca2a 100644 --- a/integrations/klaviyo/src/integration.ts +++ b/integrations/klaviyo/src/integration.ts @@ -1,6 +1,7 @@ import { Integration, type IntegrationDefinition, + isUnauthorizedStatusError, SdkException, } from "@chatbotx.io/sdk" import { z } from "zod" @@ -34,6 +35,19 @@ const pageSearchParams = (props: { cursor?: string; size: number }) => { const extractNextCursor = (next: string | null | undefined) => next ? new URL(next).searchParams.get("page[cursor]") : null +/** Shared by `connection.fromCredentials` (live-validate + return `AuthValue`) and the legacy `validateCredentials` action. */ +const buildKlaviyoAuth = async (apiKey: string): Promise => { + const auth = createKlaviyoAuth(apiKey) + await klaviyoRequest( + auth, + KLAVIYO_LISTS_PATH, + klaviyoListsResponseSchema, + { searchParams: pageSearchParams({ size: 1 }) }, + [200], + ) + return auth +} + const config: IntegrationDefinition< KlaviyoConfig, KlaviyoAuthValue, @@ -57,6 +71,8 @@ const config: IntegrationDefinition< sourceId: "workspace", displayName: "Klaviyo", }), + fromCredentials: (config: { apiKey: string }) => + buildKlaviyoAuth(config.apiKey), verify: async ({ auth }) => { try { await klaviyoRequest( @@ -70,11 +86,7 @@ const config: IntegrationDefinition< } catch (error) { return { ok: false, - revoked: - typeof error === "object" && - error !== null && - "statusCode" in error && - (error.statusCode === 401 || error.statusCode === 403), + revoked: isUnauthorizedStatusError(error), error: error instanceof Error ? error.message @@ -82,24 +94,10 @@ const config: IntegrationDefinition< } } }, - isRevokedTokenError: (error) => - typeof error === "object" && - error !== null && - "statusCode" in error && - (error.statusCode === 401 || error.statusCode === 403), + isRevokedTokenError: isUnauthorizedStatusError, }, actions: { - validateCredentials: async ({ props }) => { - const auth = createKlaviyoAuth(props.apiKey) - await klaviyoRequest( - auth, - KLAVIYO_LISTS_PATH, - klaviyoListsResponseSchema, - { searchParams: pageSearchParams({ size: 1 }) }, - [200], - ) - return auth - }, + validateCredentials: async ({ props }) => buildKlaviyoAuth(props.apiKey), listLists: async ({ ctx, props }) => { const page = klaviyoListPageInputSchema.parse(props) const response = await klaviyoRequest( diff --git a/integrations/mailchimp/__tests__/connection.test.ts b/integrations/mailchimp/__tests__/connection.test.ts new file mode 100644 index 0000000000..ecba4d9f0f --- /dev/null +++ b/integrations/mailchimp/__tests__/connection.test.ts @@ -0,0 +1,65 @@ +import { afterEach, describe, expect, test, vi } from "vitest" +import { integration } from "../src/integration" + +const jsonResponse = (body: unknown, status = 200) => + new Response(JSON.stringify(body), { + status, + headers: { "Content-Type": "application/json" }, + }) + +const connection = integration.connection +if (!connection?.fromCredentials) { + throw new Error("mailchimp integration has no connection.fromCredentials") +} + +afterEach(() => { + vi.unstubAllGlobals() +}) + +describe("mailchimp connection.fromCredentials", () => { + test("live-validates the api key and returns a custom auth value", async () => { + const fetchMock = vi.fn(async () => jsonResponse({ health_status: "ok" })) + vi.stubGlobal("fetch", fetchMock) + + const auth = await integration.connection?.fromCredentials?.({ + apiKey: " key-us1 ", + }) + + expect(auth).toMatchObject({ authType: "custom", apiKey: "key-us1" }) + expect(fetchMock).toHaveBeenCalledTimes(1) + }) + + test("rejects an invalid api key by rethrowing the ping failure", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async () => + jsonResponse({ status: 401, title: "Invalid key" }, 401), + ), + ) + + await expect( + integration.connection?.fromCredentials?.({ apiKey: "bad-key" }), + ).rejects.toBeTruthy() + }) +}) + +describe("mailchimp connection.verify / isRevokedTokenError", () => { + test("verify reports revoked:true on a 401 from the ping endpoint", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async () => + jsonResponse({ status: 401, title: "Invalid key" }, 401), + ), + ) + + const health = await connection.verify({ + auth: { authType: "custom", apiKey: "key", dataCenter: "us1" } as never, + }) + expect(health).toMatchObject({ ok: false, revoked: true }) + }) + + test("isRevokedTokenError is a real predicate, not a stub", () => { + expect(connection.isRevokedTokenError({ statusCode: 401 })).toBe(true) + expect(connection.isRevokedTokenError({ statusCode: 500 })).toBe(false) + }) +}) diff --git a/integrations/mailchimp/package.json b/integrations/mailchimp/package.json index e1150057ff..6ea9091aed 100644 --- a/integrations/mailchimp/package.json +++ b/integrations/mailchimp/package.json @@ -13,14 +13,18 @@ }, "devDependencies": { "@chatbotx.io/typescript-config": "workspace:*", + "@chatbotx.io/vitest-config": "workspace:*", "@types/node": "^24.10.4", - "typescript": "^5" + "typescript": "^5", + "vitest": "^4.1.0" }, "exports": { ".": "./src/index.ts", "./**/*": "./src/**/*.ts" }, "scripts": { - "check-types": "tsc --noEmit" + "check-types": "tsc --noEmit", + "test": "vitest run --passWithNoTests", + "test:watch": "vitest" } } diff --git a/integrations/mailchimp/src/integration.ts b/integrations/mailchimp/src/integration.ts index bd56c436a8..6f3e707981 100644 --- a/integrations/mailchimp/src/integration.ts +++ b/integrations/mailchimp/src/integration.ts @@ -1,6 +1,7 @@ import { Integration, type IntegrationDefinition, + isUnauthorizedStatusError, SdkException, } from "@chatbotx.io/sdk" import { z } from "zod" @@ -24,6 +25,19 @@ import { mailchimpTagsResponseSchema, } from "./schemas" +/** Shared by `connection.fromCredentials` (live-validate + return `AuthValue`) and the legacy `validateApiKey` action. */ +const buildMailchimpAuth = async ( + apiKey: string, +): Promise => { + const auth = createMailchimpAuth(apiKey) + await mailchimpRequest( + auth, + MAILCHIMP_PING_ENDPOINT, + mailchimpPingResponseSchema, + ) + return auth +} + const config: IntegrationDefinition< MailchimpConfig, MailchimpAuthValue, @@ -47,6 +61,8 @@ const config: IntegrationDefinition< sourceId: "workspace", displayName: "Mailchimp", }), + fromCredentials: (config: { apiKey: string }) => + buildMailchimpAuth(config.apiKey), verify: async ({ auth }) => { try { await mailchimpRequest( @@ -58,11 +74,7 @@ const config: IntegrationDefinition< } catch (error) { return { ok: false, - revoked: - typeof error === "object" && - error !== null && - "statusCode" in error && - (error.statusCode === 401 || error.statusCode === 403), + revoked: isUnauthorizedStatusError(error), error: error instanceof Error ? error.message @@ -70,20 +82,11 @@ const config: IntegrationDefinition< } } }, - isRevokedTokenError: (error) => - typeof error === "object" && - error !== null && - "statusCode" in error && - (error.statusCode === 401 || error.statusCode === 403), + isRevokedTokenError: isUnauthorizedStatusError, }, actions: { validateApiKey: async ({ props }) => { - const auth = createMailchimpAuth(props.apiKey) - await mailchimpRequest( - auth, - MAILCHIMP_PING_ENDPOINT, - mailchimpPingResponseSchema, - ) + const auth = await buildMailchimpAuth(props.apiKey) return { dataCenter: auth.dataCenter } }, listAudiences: async ({ ctx }) => { diff --git a/integrations/mailchimp/vitest.config.ts b/integrations/mailchimp/vitest.config.ts new file mode 100644 index 0000000000..84da752da3 --- /dev/null +++ b/integrations/mailchimp/vitest.config.ts @@ -0,0 +1 @@ +export { default } from "@chatbotx.io/vitest-config/node" diff --git a/integrations/mailer-lite/__tests__/connection.test.ts b/integrations/mailer-lite/__tests__/connection.test.ts new file mode 100644 index 0000000000..49f793741c --- /dev/null +++ b/integrations/mailer-lite/__tests__/connection.test.ts @@ -0,0 +1,53 @@ +import { afterEach, describe, expect, test, vi } from "vitest" +import { integration } from "../src/integration" + +const jsonResponse = (body: unknown, status = 200) => + new Response(JSON.stringify(body), { + status, + headers: { "Content-Type": "application/json" }, + }) + +const connection = integration.connection +if (!connection?.fromCredentials) { + throw new Error("mailerLite integration has no connection.fromCredentials") +} + +afterEach(() => { + vi.unstubAllGlobals() +}) + +describe("mailerLite connection.fromCredentials", () => { + test("live-validates the api key and returns a custom auth value", async () => { + const fetchMock = vi.fn(async () => + jsonResponse({ + data: [], + meta: { current_page: 1, last_page: 1, per_page: 1, total: 0 }, + }), + ) + vi.stubGlobal("fetch", fetchMock) + + const auth = await connection.fromCredentials?.({ apiKey: " key " }) + + expect(auth).toMatchObject({ authType: "custom", apiKey: "key" }) + expect(fetchMock).toHaveBeenCalledTimes(1) + }) + + test("rejects an invalid api key by rethrowing the groups-page failure", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async () => jsonResponse({}, 401)), + ) + + await expect( + connection.fromCredentials?.({ apiKey: "bad-key" }), + ).rejects.toBeTruthy() + }) +}) + +describe("mailerLite connection.isRevokedTokenError", () => { + test("is a real predicate, not a stub", () => { + expect(connection.isRevokedTokenError({ statusCode: 401 })).toBe(true) + expect(connection.isRevokedTokenError({ statusCode: 403 })).toBe(true) + expect(connection.isRevokedTokenError({ statusCode: 500 })).toBe(false) + }) +}) diff --git a/integrations/mailer-lite/src/integration.ts b/integrations/mailer-lite/src/integration.ts index 6ed10266f6..e82c5427b8 100644 --- a/integrations/mailer-lite/src/integration.ts +++ b/integrations/mailer-lite/src/integration.ts @@ -1,6 +1,7 @@ import { Integration, type IntegrationDefinition, + isUnauthorizedStatusError, SdkException, } from "@chatbotx.io/sdk" import { mailerLiteRequest } from "./client" @@ -38,6 +39,21 @@ const pageSearchParams = (props: { page: number; limit: number }) => limit: String(props.limit), }) +/** Shared by `connection.fromCredentials` (live-validate + return `AuthValue`) and the legacy `validateCredentials` action. */ +const buildMailerLiteAuth = async ( + apiKey: string, +): Promise => { + const auth = createMailerLiteAuth(apiKey) + await mailerLiteRequest( + auth, + MAILER_LITE_GROUPS_PATH, + mailerLiteGroupsResponseSchema, + { searchParams: pageSearchParams({ page: 1, limit: 1 }) }, + [200], + ) + return auth +} + const config: IntegrationDefinition< MailerLiteConfig, MailerLiteAuthValue, @@ -61,6 +77,8 @@ const config: IntegrationDefinition< sourceId: "workspace", displayName: "MailerLite", }), + fromCredentials: (config: { apiKey: string }) => + buildMailerLiteAuth(config.apiKey), verify: async ({ auth }) => { try { await mailerLiteRequest( @@ -74,11 +92,7 @@ const config: IntegrationDefinition< } catch (error) { return { ok: false, - revoked: - typeof error === "object" && - error !== null && - "statusCode" in error && - (error.statusCode === 401 || error.statusCode === 403), + revoked: isUnauthorizedStatusError(error), error: error instanceof Error ? error.message @@ -86,24 +100,10 @@ const config: IntegrationDefinition< } } }, - isRevokedTokenError: (error) => - typeof error === "object" && - error !== null && - "statusCode" in error && - (error.statusCode === 401 || error.statusCode === 403), + isRevokedTokenError: isUnauthorizedStatusError, }, actions: { - validateCredentials: async ({ props }) => { - const auth = createMailerLiteAuth(props.apiKey) - await mailerLiteRequest( - auth, - MAILER_LITE_GROUPS_PATH, - mailerLiteGroupsResponseSchema, - { searchParams: pageSearchParams({ page: 1, limit: 1 }) }, - [200], - ) - return auth - }, + validateCredentials: async ({ props }) => buildMailerLiteAuth(props.apiKey), listGroups: async ({ ctx, props }) => { const response = await mailerLiteRequest( ctx.auth, diff --git a/integrations/messenger/__tests__/connection-oauth.test.ts b/integrations/messenger/__tests__/connection-oauth.test.ts index cab1afa154..c808b1cb03 100644 --- a/integrations/messenger/__tests__/connection-oauth.test.ts +++ b/integrations/messenger/__tests__/connection-oauth.test.ts @@ -77,7 +77,7 @@ describe("Messenger connection.exchangeCode", () => { authType: "oauth2", clientId: "client-1", clientSecret: "secret-1", - redirectUrl: "", + redirectUrl: "https://app.example.test/callback", version: "v23.0", tokens: { accessToken: "long-lived-token" }, }) diff --git a/integrations/messenger/src/apis/auth.ts b/integrations/messenger/src/apis/auth.ts index 0eddf45d0a..fb6df0a33b 100644 --- a/integrations/messenger/src/apis/auth.ts +++ b/integrations/messenger/src/apis/auth.ts @@ -324,6 +324,41 @@ export function debugToken({ }) } +/** + * Shared `ConnectionProvider.verify` body for a Meta-graph token: inspects + * it via `debugToken` and maps the result to a `ConnectionHealth`. `label` + * only varies the error string (`"${label} access token is invalid"`) — + * Messenger and Instagram-via-Facebook run byte-identical logic otherwise. + */ +export const verifyMetaToken = + (label: string) => + async ({ + auth, + }: { + auth: { + clientId: string + clientSecret: string + tokens: { accessToken: string; expiresAt?: string } + metadata: { version: string } + } + }) => { + const token = await debugToken({ + inputToken: auth.tokens.accessToken, + appAccessToken: toAppAccessToken(auth), + version: auth.metadata.version, + }) + + if (token.is_valid !== true) { + return { + ok: false as const, + revoked: true, + error: `${label} access token is invalid`, + } + } + + return { ok: true as const, authExpiresAt: auth.tokens.expiresAt } + } + export function hasLeadsRetrieval(scopes: string[] | undefined): boolean { return Boolean(scopes?.includes(LEADS_RETRIEVAL_SCOPE)) } diff --git a/integrations/messenger/src/integration.ts b/integrations/messenger/src/integration.ts index fe1cd2489a..0adeb48262 100644 --- a/integrations/messenger/src/integration.ts +++ b/integrations/messenger/src/integration.ts @@ -5,11 +5,11 @@ import { type IntegrationDefinition, } from "@chatbotx.io/sdk" import { - debugToken, exchangeCodeForToken, getUserPages, MESSENGER_SCOPES, toAppAccessToken, + verifyMetaToken, } from "./apis/auth" import { getCommentAttachment, @@ -102,7 +102,11 @@ const config: IntegrationDefinition< authType: AuthType.oauth2, clientId: config.clientId, clientSecret: config.clientSecret, - redirectUrl: "", + // The real callback URL `authorizeUrl` sent as `redirect_uri` — + // `oauth2AuthSchema.redirectUrl` is `min(1)`; a hardcoded `""` here + // fails the first generic validator that parses this value (Google + // Calendar's `.extend()` pattern already does). + redirectUrl: callbackUrl, version: config.version, tokens: { accessToken: longLivedToken }, } @@ -125,7 +129,7 @@ const config: IntegrationDefinition< authType: AuthType.oauth2, clientId: auth.clientId, clientSecret: auth.clientSecret, - redirectUrl: "", + redirectUrl: auth.redirectUrl, version, tokens: { accessToken: page.access_token as string }, metadata: { pageId: page.id, pageName: page.name, version }, @@ -136,23 +140,7 @@ const config: IntegrationDefinition< sourceId: auth.metadata.pageId, displayName: auth.metadata.pageName, }), - verify: async ({ auth }) => { - const token = await debugToken({ - inputToken: auth.tokens.accessToken, - appAccessToken: toAppAccessToken(auth), - version: auth.metadata.version, - }) - - if (token.is_valid !== true) { - return { - ok: false, - revoked: true, - error: "Messenger access token is invalid", - } - } - - return { ok: true, authExpiresAt: auth.tokens.expiresAt } - }, + verify: verifyMetaToken("Messenger"), isRevokedTokenError, webhook: { subscribe: ({ auth }) => diff --git a/integrations/moosend/__tests__/connection.test.ts b/integrations/moosend/__tests__/connection.test.ts new file mode 100644 index 0000000000..4c2013a89a --- /dev/null +++ b/integrations/moosend/__tests__/connection.test.ts @@ -0,0 +1,55 @@ +import { afterEach, describe, expect, test, vi } from "vitest" +import { integration } from "../src/integration" + +const listResponse = (mailingLists: unknown[] = []) => ({ + Code: 0, + Error: null, + Context: { + Paging: { + PageSize: 1, + CurrentPage: 1, + TotalResults: mailingLists.length, + TotalPageCount: mailingLists.length > 0 ? 1 : 0, + }, + MailingLists: mailingLists, + }, +}) + +const errorResponse = (code: number) => ({ + Code: code, + Error: "Invalid ApiKey", + Context: null, +}) + +const connection = integration.connection +if (!connection?.fromCredentials) { + throw new Error("moosend integration has no connection.fromCredentials") +} + +afterEach(() => { + vi.unstubAllGlobals() +}) + +describe("moosend connection.fromCredentials", () => { + test("live-validates the api key and returns a custom auth value", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async () => Response.json(listResponse())), + ) + + const auth = await connection.fromCredentials?.({ apiKey: " key " }) + + expect(auth).toMatchObject({ authType: "custom", apiKey: "key" }) + }) + + test("rejects an invalid api key by rethrowing the lists failure", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async () => Response.json(errorResponse(1), { status: 401 })), + ) + + await expect( + connection.fromCredentials?.({ apiKey: "bad-key" }), + ).rejects.toBeTruthy() + }) +}) diff --git a/integrations/moosend/src/integration.ts b/integrations/moosend/src/integration.ts index 9847120040..c460a47d1a 100644 --- a/integrations/moosend/src/integration.ts +++ b/integrations/moosend/src/integration.ts @@ -16,6 +16,17 @@ import { moosendSubscriberResponseSchema, } from "./schemas" +/** Shared by `connection.fromCredentials` (live-validate + return `AuthValue`) and the legacy `validateCredentials` action. */ +const buildMoosendAuth = async (apiKey: string): Promise => { + const auth = createMoosendAuth(apiKey) + await moosendRequest( + auth, + moosendListsPagePath(1, 1), + moosendMailingListsResponseSchema, + ) + return auth +} + const config: IntegrationDefinition< MoosendConfig, MoosendAuthValue, @@ -39,6 +50,8 @@ const config: IntegrationDefinition< sourceId: "workspace", displayName: "Moosend", }), + fromCredentials: (config: { apiKey: string }) => + buildMoosendAuth(config.apiKey), verify: async ({ auth }) => { try { await moosendRequest( @@ -69,15 +82,7 @@ const config: IntegrationDefinition< error.kind === "invalid_credentials", }, actions: { - validateCredentials: async ({ props }) => { - const auth = createMoosendAuth(props.apiKey) - await moosendRequest( - auth, - moosendListsPagePath(1, 1), - moosendMailingListsResponseSchema, - ) - return auth - }, + validateCredentials: async ({ props }) => buildMoosendAuth(props.apiKey), listMailingLists: async ({ ctx, props }) => { const page = moosendListPageRequestSchema.parse(props) const response = await moosendRequest( diff --git a/integrations/sendgrid/__tests__/connection.test.ts b/integrations/sendgrid/__tests__/connection.test.ts new file mode 100644 index 0000000000..13ce487c9f --- /dev/null +++ b/integrations/sendgrid/__tests__/connection.test.ts @@ -0,0 +1,60 @@ +import { afterEach, describe, expect, test, vi } from "vitest" +import { integration } from "../src/integration" + +const jsonResponse = (body: unknown, status = 200) => + new Response(JSON.stringify(body), { + status, + headers: { "Content-Type": "application/json" }, + }) + +const connection = integration.connection +if (!connection?.fromCredentials) { + throw new Error("sendGrid integration has no connection.fromCredentials") +} + +afterEach(() => { + vi.unstubAllGlobals() +}) + +describe("sendGrid connection.fromCredentials", () => { + test("live-validates the api key and returns a custom auth value", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async () => jsonResponse({ scopes: ["marketing.read"] })), + ) + + const auth = await connection.fromCredentials?.({ apiKey: " key " }) + + expect(auth).toMatchObject({ authType: "custom", apiKey: "key" }) + }) + + test("rejects a key missing marketing read scope", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async () => jsonResponse({ scopes: [] })), + ) + + await expect( + connection.fromCredentials?.({ apiKey: "key" }), + ).rejects.toBeTruthy() + }) + + test("rejects an invalid api key by rethrowing the scopes failure", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async () => jsonResponse({}, 401)), + ) + + await expect( + connection.fromCredentials?.({ apiKey: "bad-key" }), + ).rejects.toBeTruthy() + }) +}) + +describe("sendGrid connection.isRevokedTokenError", () => { + test("is a real predicate, not a stub", () => { + expect(connection.isRevokedTokenError({ statusCode: 401 })).toBe(true) + expect(connection.isRevokedTokenError({ statusCode: 403 })).toBe(true) + expect(connection.isRevokedTokenError({ statusCode: 500 })).toBe(false) + }) +}) diff --git a/integrations/sendgrid/src/integration.ts b/integrations/sendgrid/src/integration.ts index 4df8f8a232..0fca50005a 100644 --- a/integrations/sendgrid/src/integration.ts +++ b/integrations/sendgrid/src/integration.ts @@ -1,6 +1,7 @@ import { Integration, type IntegrationDefinition, + isUnauthorizedStatusError, SdkException, } from "@chatbotx.io/sdk" import { sendGridRequest } from "./client" @@ -37,6 +38,30 @@ const getNextPageToken = (next?: string) => { return url.searchParams.get("page_token")?.trim() || undefined } +/** Shared by `connection.fromCredentials` (live-validate + return `AuthValue`) and the legacy `validateCredentials` action. */ +const buildSendGridAuth = async ( + apiKey: string, +): Promise => { + const auth = createSendGridAuth(apiKey) + const { scopes } = await sendGridRequest( + auth, + SENDGRID_SCOPES_PATH, + sendGridScopesResponseSchema, + ) + // SendGrid API keys can report Marketing permissions under either the + // modern "marketing.*" scope names or the legacy "marketing_campaigns.*" + // names depending on key type. Full Access keys have implicit write + // access but do NOT enumerate "marketing.write" in the scopes endpoint + // even though write calls succeed (HTTP 202). Checking read is enough. + const hasRead = + scopes.includes("marketing.read") || + scopes.includes("marketing_campaigns.read") + if (!hasRead) { + throw new SendGridMissingScopesError(["marketing.read"]) + } + return auth +} + const config: IntegrationDefinition< SendGridConfig, SendGridAuthValue, @@ -60,6 +85,8 @@ const config: IntegrationDefinition< sourceId: "workspace", displayName: "SendGrid", }), + fromCredentials: (config: { apiKey: string }) => + buildSendGridAuth(config.apiKey), verify: async ({ auth }) => { try { await sendGridRequest( @@ -71,11 +98,7 @@ const config: IntegrationDefinition< } catch (error) { return { ok: false, - revoked: - typeof error === "object" && - error !== null && - "statusCode" in error && - (error.statusCode === 401 || error.statusCode === 403), + revoked: isUnauthorizedStatusError(error), error: error instanceof Error ? error.message @@ -83,33 +106,10 @@ const config: IntegrationDefinition< } } }, - isRevokedTokenError: (error) => - typeof error === "object" && - error !== null && - "statusCode" in error && - (error.statusCode === 401 || error.statusCode === 403), + isRevokedTokenError: isUnauthorizedStatusError, }, actions: { - validateCredentials: async ({ props }) => { - const auth = createSendGridAuth(props.apiKey) - const { scopes } = await sendGridRequest( - auth, - SENDGRID_SCOPES_PATH, - sendGridScopesResponseSchema, - ) - // SendGrid API keys can report Marketing permissions under either the - // modern "marketing.*" scope names or the legacy "marketing_campaigns.*" - // names depending on key type. Full Access keys have implicit write - // access but do NOT enumerate "marketing.write" in the scopes endpoint - // even though write calls succeed (HTTP 202). Checking read is enough. - const hasRead = - scopes.includes("marketing.read") || - scopes.includes("marketing_campaigns.read") - if (!hasRead) { - throw new SendGridMissingScopesError(["marketing.read"]) - } - return auth - }, + validateCredentials: async ({ props }) => buildSendGridAuth(props.apiKey), listLists: async ({ ctx, props }) => { const searchParams = new URLSearchParams({ page_size: String(props.pageSize), diff --git a/integrations/telegram/__tests__/connection.test.ts b/integrations/telegram/__tests__/connection.test.ts new file mode 100644 index 0000000000..a87056bc79 --- /dev/null +++ b/integrations/telegram/__tests__/connection.test.ts @@ -0,0 +1,88 @@ +import { describe, expect, test, vi } from "vitest" + +const mocks = vi.hoisted(() => ({ + connect: vi.fn(), + getMe: vi.fn(), + deleteWebhook: vi.fn(), + registerWebhook: vi.fn(), +})) + +vi.mock("../src/apis/bot", () => ({ + connect: mocks.connect, + getMe: mocks.getMe, + deleteWebhook: mocks.deleteWebhook, + registerWebhook: mocks.registerWebhook, +})) + +const { integration } = await import("../src/integration") +const connection = integration.connection + +if (!connection) { + throw new Error("telegram integration has no connection block") +} + +describe("telegram connection.describe", () => { + test("derives sourceId from the bot-id prefix of the token, not a constant", () => { + const descriptor = connection.describe({ + authType: "secretText", + secretText: "123456789:AAExampleSecretPart", + }) + expect(descriptor.sourceId).toBe("123456789") + }) + + test("two different bot tokens never collapse to the same sourceId", () => { + const first = connection.describe({ + authType: "secretText", + secretText: "111:secret-a", + }) + const second = connection.describe({ + authType: "secretText", + secretText: "222:secret-b", + }) + expect(first.sourceId).not.toBe(second.sourceId) + }) +}) + +describe("telegram connection.fromCredentials", () => { + test("live-validates the bot token and returns a secretText auth value", async () => { + mocks.connect.mockResolvedValue({ id: "123456789", username: "my_bot" }) + if (!connection.fromCredentials) { + throw new Error("telegram connection has no fromCredentials") + } + const auth = await connection.fromCredentials({ + secretText: "123456789:AAExampleSecretPart", + }) + expect(mocks.connect).toHaveBeenCalledWith({ + botToken: "123456789:AAExampleSecretPart", + }) + expect(auth).toEqual({ + authType: "secretText", + secretText: "123456789:AAExampleSecretPart", + }) + }) + + test("rejects an invalid bot token by rethrowing the getMe failure", async () => { + mocks.connect.mockRejectedValue(new Error("Unauthorized")) + if (!connection.fromCredentials) { + throw new Error("telegram connection has no fromCredentials") + } + await expect( + connection.fromCredentials({ secretText: "bad-token" }), + ).rejects.toThrow("Unauthorized") + }) +}) + +describe("telegram connection.verify", () => { + const auth = { authType: "secretText" as const, secretText: "123:secret" } + + test("returns ok:true when getMe succeeds", async () => { + mocks.getMe.mockResolvedValue({ id: "123", username: "my_bot" }) + await expect(connection.verify({ auth })).resolves.toEqual({ ok: true }) + }) + + test("returns a ConnectionHealth failure instead of throwing when the token is revoked", async () => { + mocks.getMe.mockRejectedValue(new Error("Unauthorized")) + const health = await connection.verify({ auth }) + expect(health.ok).toBe(false) + }) +}) diff --git a/integrations/telegram/src/integration.ts b/integrations/telegram/src/integration.ts index 363841463e..c76890f935 100644 --- a/integrations/telegram/src/integration.ts +++ b/integrations/telegram/src/integration.ts @@ -53,14 +53,37 @@ const config: IntegrationDefinition< labelKey: "integrations.telegram.fields.secretText", }, ], - describe: () => ({ - // TelegramAuthValue stores only the bot token; the bot ID lives outside auth. - sourceId: "workspace", + describe: (auth) => ({ + // Telegram bot tokens are formatted `:` — the numeric + // prefix is Telegram's own stable bot identity: it matches what the + // Phase 1 backfill wrote into `IntegrationTelegram.botId` (this + // provider's `identityColumn`) and what `ConnectionStoreBinding + // .insertRow` writes back into that same NOT NULL unique column from + // this exact `sourceId`. A constant like `"workspace"` would collide + // across every bot in the workspace and corrupt `botId`. + sourceId: auth.secretText.split(":")[0] || auth.secretText, displayName: "Telegram bot", }), + fromCredentials: async (config: { secretText: string }) => { + // Live-validates the bot token via the same `getMe` call the legacy + // `actions.connect` handler uses. + await connect({ botToken: config.secretText }) + return { authType: "secretText", secretText: config.secretText } + }, verify: async ({ auth }) => { - await getMe(auth) - return { ok: true } + try { + await getMe(auth) + return { ok: true } + } catch (error) { + return { + ok: false, + revoked: isRevokedTokenError(error), + error: + error instanceof Error + ? error.message + : "Telegram bot token verification failed", + } + } }, isRevokedTokenError, }, diff --git a/packages/business/__tests__/inbox-status-write-guard.test.ts b/packages/business/__tests__/inbox-status-write-guard.test.ts new file mode 100644 index 0000000000..5ee777f077 --- /dev/null +++ b/packages/business/__tests__/inbox-status-write-guard.test.ts @@ -0,0 +1,65 @@ +// @vitest-environment node + +/** + * Pins the plan's "single writer" invariant for `Inbox.status`: every + * channel-active/inactive transition should flow through the `Connection` + * FSM (`ConnectionStateService.transition`'s `mirrorInboxStatus`), not a + * bespoke direct `.update(inboxModel)` — otherwise `Connection.status` and + * `Inbox.status` silently drift (see the messenger/instagram disconnect + * billing bug this guard exists to catch a repeat of). + * + * Allow-listed exceptions, both pre-existing and intentional: + * - `connection/state-service.ts` — the one legitimate writer. + * - `inbox/service.ts` — the legacy `create`/`disconnect` API kept for + * every not-yet-migrated channel (telegram, zalo, whatsapp, tiktok, + * webchat, smtp, api); Phase 5 removes this exception. + * - `integration-context/auth-store.ts` — the pre-backfill fallback for + * an `Integration` row with no `Connection` counterpart yet. + * + * A new match outside this list means a new bypass was just introduced — + * route it through `connectionStateService`/`inboxService` instead of + * widening the allow-list. + */ + +import { readFileSync } from "node:fs" +import { glob } from "node:fs/promises" +import { resolve } from "node:path" +import { describe, expect, test } from "vitest" + +const SRC_ROOT = resolve(import.meta.dirname, "../src") + +const ALLOWED_WRITERS = new Set([ + "connection/state-service.ts", + "inbox/service.ts", + "integration-context/auth-store.ts", +]) + +const DIRECT_INBOX_UPDATE_REGEX = /\.update\(\s*inboxModel\s*\)/ + +describe("Inbox.status write guard", () => { + test("only the allow-listed services write to inboxModel directly", async () => { + const offenders: string[] = [] + + for await (const entry of glob("**/*.ts", { cwd: SRC_ROOT })) { + if (entry.endsWith(".test.ts")) { + continue + } + const source = readFileSync(resolve(SRC_ROOT, entry), "utf8") + if ( + DIRECT_INBOX_UPDATE_REGEX.test(source) && + !ALLOWED_WRITERS.has(entry) + ) { + offenders.push(entry) + } + } + + expect(offenders).toEqual([]) + }) + + test("every allow-listed exception still exists and still writes inboxModel", () => { + for (const relativePath of ALLOWED_WRITERS) { + const source = readFileSync(resolve(SRC_ROOT, relativePath), "utf8") + expect(DIRECT_INBOX_UPDATE_REGEX.test(source)).toBe(true) + } + }) +}) diff --git a/packages/business/src/connect-session/service.ts b/packages/business/src/connect-session/service.ts index 3ab0054a60..8fa1083a35 100644 --- a/packages/business/src/connect-session/service.ts +++ b/packages/business/src/connect-session/service.ts @@ -121,6 +121,16 @@ class ConnectSessionService extends BaseService { stateNonceHash, status: "pending", step: "authorize", + // The schema declares `.default(sql\`[]\`)` for these four columns, but + // drizzle-kit never inlines a `sql` default into the generated + // migration (see `schema-default-parity.test.ts`) — the physical + // columns have NO database default, so omitting any of these turns + // into a bare `DEFAULT` keyword and a NOT NULL violation. Every insert + // must write them explicitly. + targets: [], + claimedTargetIds: [], + resultConnectionIds: [], + results: [], expiresAt: new Date(Date.now() + PENDING_TTL_MS), }) diff --git a/packages/business/src/connection/__tests__/state-service.test.ts b/packages/business/src/connection/__tests__/state-service.test.ts index bd892058c2..bbc9f6ab81 100644 --- a/packages/business/src/connection/__tests__/state-service.test.ts +++ b/packages/business/src/connection/__tests__/state-service.test.ts @@ -1,5 +1,7 @@ import { beforeEach, describe, expect, test, vi } from "vitest" +const NO_OWNER_ID_MESSAGE = /no ownerId/ + const mocks = vi.hoisted(() => ({ findById: vi.fn(), findByProviderAndSourceIdAnyWorkspace: vi.fn(), @@ -28,6 +30,9 @@ vi.mock("@chatbotx.io/database/repositories", () => ({ vi.mock("@chatbotx.io/database/client", () => ({ db: { update: mocks.inboxUpdate, + transaction: vi.fn(async (fn: (tx: unknown) => unknown) => + fn({ update: mocks.inboxUpdate }), + ), }, eq: vi.fn((column, value) => ({ column, value })), })) @@ -153,6 +158,36 @@ describe("ConnectionStateService.transition", () => { expect(mocks.tryConsume).not.toHaveBeenCalled() expect(mocks.release).not.toHaveBeenCalled() }) + + test("throws channelLimitReached and never writes status when quota consume fails (I5)", async () => { + mocks.findById.mockResolvedValue(baseConnection({ status: "needs_reauth" })) + mocks.tryConsume.mockResolvedValueOnce({ ok: false }) + + await expect( + connectionStateService.transition({ + connectionId: "conn-1", + event: "connect.completed", + ownerId: "owner-1", + }), + ).rejects.toMatchObject({ code: "channelLimitReached" }) + + expect(mocks.update).not.toHaveBeenCalled() + expect(mocks.inboxUpdateSet).not.toHaveBeenCalled() + }) + + test("throws when a channel quotaEdge is required but no ownerId is supplied (I8)", async () => { + mocks.findById.mockResolvedValue(baseConnection({ status: "needs_reauth" })) + + await expect( + connectionStateService.transition({ + connectionId: "conn-1", + event: "connect.completed", + }), + ).rejects.toThrow(NO_OWNER_ID_MESSAGE) + + expect(mocks.update).not.toHaveBeenCalled() + expect(mocks.tryConsume).not.toHaveBeenCalled() + }) }) describe("ConnectionStateService.markUnhealthy", () => { diff --git a/packages/business/src/connection/credential-providers.ts b/packages/business/src/connection/credential-providers.ts index 8eddf73b71..45dfa62d57 100644 --- a/packages/business/src/connection/credential-providers.ts +++ b/packages/business/src/connection/credential-providers.ts @@ -117,10 +117,15 @@ export const openaiCompatibleConnectionProvider: ConnectionProvider< return secretTextAuth(apiKey) }, verify: ({ auth }) => { - // `auth` alone has no `baseURL` (it lives on the DB row's own column, not - // the auth jsonb) — a real health check needs that row's `baseURL` - // alongside `auth.secretText`, wired in Phase 2 via `ConnectionService`. - // Until then, treat presence of a stored secret as sufficient. + // `auth` alone has no `baseURL` (it lives on the DB row's own column, + // not the auth jsonb) — a real health check needs that row's + // `baseURL` alongside `auth.secretText`. `ConnectionProvider.verify`'s + // signature (`Handler<{auth}, ConnectionHealth>`) has no room for it: + // `ConnectionService.verify`/`ConnectionService.refresh` only ever load + // and pass `auth`, never the satellite row's other columns, for any + // provider. Still unwired — treat presence of a stored secret as + // sufficient until either `baseURL` moves into `auth` itself or + // `verify`'s signature grows a second, store-row parameter. return Promise.resolve( auth.secretText ? { ok: true as const } diff --git a/packages/business/src/connection/state-service.ts b/packages/business/src/connection/state-service.ts index bc964c0ee5..e55e790fd9 100644 --- a/packages/business/src/connection/state-service.ts +++ b/packages/business/src/connection/state-service.ts @@ -12,6 +12,7 @@ import { import { inboxModel } from "@chatbotx.io/database/schema" import type { ConnectionModel } from "@chatbotx.io/database/types" import { BaseService } from "../base.service" +import { channelLimitReachedException } from "../errors" import { logger } from "../logger" import { quotaEnforcementService } from "../quota-enforcement/service" import { workspaceUsageService } from "../workspace-usage/service" @@ -101,65 +102,121 @@ class ConnectionStateService extends BaseService { ownerId?: string tx?: DatabaseClient }): Promise { - const client = input.tx ?? db - const existing = await connectionRepository.findById( - { id: input.connectionId }, - client, - ) - if (!existing) { - throw new ConnectionNotFoundException(input.connectionId) - } + const run = async (client: DatabaseClient): Promise => { + const existing = await connectionRepository.findById( + { id: input.connectionId }, + client, + ) + if (!existing) { + throw new ConnectionNotFoundException(input.connectionId) + } - const result = transitionConnection({ - from: existing.status, - event: input.event, - reason: input.reason, - }) + const result = transitionConnection({ + from: existing.status, + event: input.event, + reason: input.reason, + }) - const updated = await connectionRepository.update( - { - id: existing.id, - values: { - status: result.to, - statusReason: result.reason, - connectedAt: - result.quotaEdge === "consume" ? new Date() : existing.connectedAt, - disconnectedAt: - result.quotaEdge === "release" - ? new Date() - : existing.disconnectedAt, - }, - }, - client, - ) - if (!updated) { - throw new ConnectionNotFoundException(input.connectionId) - } + // Gated on `kind === "channel"` here (not just `input.ownerId` being + // set) as defense-in-depth: the "channels" quota metric only applies + // to channel connections, so a caller that mistakenly resolves and + // passes an `ownerId` for a workspace-integration/sub-connection row + // (AI providers, marketing tools) still cannot mis-consume/release a + // channel-quota slot. + if (result.quotaEdge && existing.kind === "channel" && !input.ownerId) { + // A channel-kind connection crossing the active/inactive boundary + // with no resolvable owner is a caller bug, not a legitimate no-op + // — `inboxService.create` throws in the same situation. Silently + // skipping here would let a channel go `connected` (or a disconnect + // go through) with no quota consumed/released at all. + throw new Error( + `connection ${existing.id} transition "${input.event}" would ${result.quotaEdge} channel quota but no ownerId was supplied`, + ) + } - if (existing.inboxId) { - await this.mirrorInboxStatus({ - inboxId: existing.inboxId, - to: result.to, - reason: result.reason, - tx: client, - }) - } + // Consume BEFORE the status write: a failed consume must never + // require rolling back an already-committed transition — nothing has + // been written yet at this point. + if ( + result.quotaEdge === "consume" && + input.ownerId && + existing.kind === "channel" + ) { + const consumed = await quotaEnforcementService.tryConsume({ + userId: input.ownerId, + metric: "channels", + }) + if (!consumed.ok) { + throw channelLimitReachedException() + } + } - // Gated on `kind === "channel"` here (not just `input.ownerId` being - // set) as defense-in-depth: the "channels" quota metric only applies to - // channel connections, so a caller that mistakenly resolves and passes - // an `ownerId` for a workspace-integration/sub-connection row (AI - // providers, marketing tools) still cannot mis-consume/release a - // channel-quota slot. - if (result.quotaEdge && input.ownerId && existing.kind === "channel") { - await this.applyQuotaEdge( - result.quotaEdge, - input.ownerId, - existing.workspaceId, + const updated = await connectionRepository.update( + { + id: existing.id, + values: { + status: result.to, + statusReason: result.reason, + connectedAt: + result.quotaEdge === "consume" + ? new Date() + : existing.connectedAt, + disconnectedAt: + result.quotaEdge === "release" + ? new Date() + : existing.disconnectedAt, + }, + }, + client, ) + if (!updated) { + throw new ConnectionNotFoundException(input.connectionId) + } + + if (existing.inboxId) { + await this.mirrorInboxStatus({ + inboxId: existing.inboxId, + to: result.to, + reason: result.reason, + tx: client, + }) + } + + if ( + result.quotaEdge === "consume" && + input.ownerId && + existing.kind === "channel" + ) { + // The authoritative counter already moved above; this is the + // display-only mirror, best-effort like every other usage-counter + // write in this class. + await workspaceUsageService + .increment(existing.workspaceId, "channels") + .catch((err) => { + logger.warn( + { + err, + workspaceId: existing.workspaceId, + ownerId: input.ownerId, + }, + "connection connect: workspace usage channel increment failed", + ) + }) + } else if ( + result.quotaEdge === "release" && + input.ownerId && + existing.kind === "channel" + ) { + await this.releaseQuotaEdge(input.ownerId, existing.workspaceId) + } + + return updated } - return updated + if (input.tx) { + return await run(input.tx) + } + return await db.transaction(run) } /** @@ -259,45 +316,18 @@ class ConnectionStateService extends BaseService { } /** - * Consumes/releases exactly one unit of the owner's `channels` enforcement - * quota, then best-effort mirrors the SAME edge onto the workspace's - * display-only `channels` usage counter — the counterpart to - * `inboxService.create`'s `!skipQuota` branch and `inboxService - * .disconnect`'s unconditional decrement, both of which this method's - * callers (`connectCandidate`/`connectFromCredentials`'s revive path via - * `skipQuota: true`, and disconnect via `mirrorInboxStatus` instead of - * `inboxService.disconnect`) deliberately bypass so this is the only - * place either counter moves for a Connection-domain channel. + * Best-effort release of one unit of the owner's `channels` enforcement + * quota, then a best-effort mirror onto the workspace's display-only + * `channels` usage counter. Consume is handled inline in `transition` + * (it must run BEFORE the status write and throw on failure, unlike + * release, which never blocks/rolls back an already-inactive connection) + * — this is the release-only counterpart, the same "only place either + * counter moves for a Connection-domain channel" as before. */ - private async applyQuotaEdge( - edge: "consume" | "release", + private async releaseQuotaEdge( ownerId: string, workspaceId: string, ): Promise { - if (edge === "consume") { - const consumed = await quotaEnforcementService.tryConsume({ - userId: ownerId, - metric: "channels", - }) - if (!consumed.ok) { - // The caller already committed the status write; a failed consume - // here means the FSM and the quota ledger disagree. Surface it - // rather than silently under-counting — the nightly reconcile - // (Phase 4) is the long-term fix for this race. - throw new Error( - `connection quota consume failed for owner ${ownerId} after status transition committed`, - ) - } - await workspaceUsageService - .increment(workspaceId, "channels") - .catch((err) => { - logger.warn( - { err, workspaceId, ownerId }, - "connection connect: workspace usage channel increment failed", - ) - }) - return - } await quotaEnforcementService.release({ userId: ownerId, metric: "channels", diff --git a/packages/business/src/integration-context/__tests__/auth-store.test.ts b/packages/business/src/integration-context/__tests__/auth-store.test.ts index 595756623e..b9f8920adc 100644 --- a/packages/business/src/integration-context/__tests__/auth-store.test.ts +++ b/packages/business/src/integration-context/__tests__/auth-store.test.ts @@ -1,3 +1,4 @@ +import { AuthException } from "@chatbotx.io/sdk" import { beforeEach, describe, expect, it, vi } from "vitest" const mocks = vi.hoisted(() => ({ @@ -116,7 +117,7 @@ describe("makeAuthStore.save", () => { }) describe("makeAuthStore.markOffline", () => { - it("routes through connectionStateService.markUnhealthy with the resolved owner", async () => { + it("routes through connectionStateService.markUnhealthy for an AuthException", async () => { mocks.findByInboxId.mockResolvedValue({ id: "conn-1", workspaceId: "ws-1", @@ -125,30 +126,77 @@ describe("makeAuthStore.markOffline", () => { id: "row-1", inboxId: "inbox-1", }) - await store.markOffline?.() + await store.markOffline?.(new AuthException("revoked")) expect(mocks.markUnhealthy).toHaveBeenCalledWith({ connectionId: "conn-1", ownerId: "owner-1", }) + expect(mocks.transition).not.toHaveBeenCalled() + expect(mocks.inboxUpdate).not.toHaveBeenCalled() + }) + + it("degrades via refresh.transient_failure instead of revoking for a non-auth error", async () => { + mocks.findByInboxId.mockResolvedValue({ + id: "conn-1", + workspaceId: "ws-1", + }) + const store = makeAuthStore("messenger", { + id: "row-1", + inboxId: "inbox-1", + }) + await store.markOffline?.(new Error("ECONNRESET")) + expect(mocks.markUnhealthy).not.toHaveBeenCalled() + expect(mocks.transition).toHaveBeenCalledWith({ + connectionId: "conn-1", + event: "refresh.transient_failure", + reason: "refresh_failed", + ownerId: "owner-1", + }) expect(mocks.inboxUpdate).not.toHaveBeenCalled() }) - it("falls back to a direct Inbox write when no Connection row exists yet", async () => { + it("swallows an invalid transition when the connection is already inactive", async () => { + mocks.findByInboxId.mockResolvedValue({ + id: "conn-1", + workspaceId: "ws-1", + }) + mocks.transition.mockRejectedValueOnce(new Error("invalid transition")) + const store = makeAuthStore("messenger", { + id: "row-1", + inboxId: "inbox-1", + }) + await expect( + store.markOffline?.(new Error("ECONNRESET")), + ).resolves.toBeUndefined() + }) + + it("falls back to a direct Inbox write for an AuthException when no Connection row exists yet", async () => { mocks.findByInboxId.mockResolvedValue(undefined) const store = makeAuthStore("messenger", { id: "row-1", inboxId: "inbox-1", }) - await store.markOffline?.() + await store.markOffline?.(new AuthException("revoked")) expect(mocks.markUnhealthy).not.toHaveBeenCalled() expect(mocks.inboxUpdateSet).toHaveBeenCalledWith({ status: "disconnected", }) }) + it("does not touch Inbox for a transient failure when no Connection row exists yet", async () => { + mocks.findByInboxId.mockResolvedValue(undefined) + const store = makeAuthStore("messenger", { + id: "row-1", + inboxId: "inbox-1", + }) + await store.markOffline?.(new Error("ECONNRESET")) + expect(mocks.markUnhealthy).not.toHaveBeenCalled() + expect(mocks.inboxUpdate).not.toHaveBeenCalled() + }) + it("no-ops for a workspace-level integration with no Connection and no inboxId", async () => { const store = makeAuthStoreForTableFallback() - await store.markOffline?.() + await store.markOffline?.(new AuthException("revoked")) expect(mocks.markUnhealthy).not.toHaveBeenCalled() expect(mocks.inboxUpdate).not.toHaveBeenCalled() }) diff --git a/packages/business/src/integration-context/auth-store.ts b/packages/business/src/integration-context/auth-store.ts index b705157ed3..f3324b753e 100644 --- a/packages/business/src/integration-context/auth-store.ts +++ b/packages/business/src/integration-context/auth-store.ts @@ -3,6 +3,7 @@ import { connectionRepository } from "@chatbotx.io/database/repositories" import { inboxModel } from "@chatbotx.io/database/schema" import { distributedLock } from "@chatbotx.io/redis" import { + AuthException, type AuthStore, type AuthValue, type ConnectionHealth, @@ -109,23 +110,43 @@ export const makeAuthStoreForTable = ( timeoutInSeconds: REFRESH_LOCK_TIMEOUT_SECONDS, fn, }), - markOffline: async () => { + markOffline: async (reason?: unknown) => { const connection = await resolveConnection() + const isRevoked = reason instanceof AuthException if (connection) { const ownerId = await workspaceMemberService.findOwnerUserIdByWorkspaceId({ workspaceId: connection.workspaceId, }) - await connectionStateService.markUnhealthy({ - connectionId: connection.id, - ownerId, - }) + if (isRevoked) { + await connectionStateService.markUnhealthy({ + connectionId: connection.id, + ownerId, + }) + return + } + // Transient failure (network/5xx, retries exhausted) — degrade + // instead of revoking: the channel keeps sending and its quota slot + // stays held, matching a live provider outage rather than a real + // reauth requirement. Invalid from a terminal status (e.g. already + // `needs_reauth`) is a legitimate no-op, not an error. + try { + await connectionStateService.transition({ + connectionId: connection.id, + event: "refresh.transient_failure", + reason: "refresh_failed", + ownerId, + }) + } catch { + // Not currently active — nothing to degrade. + } return } // Pre-backfill fallback: no `Connection` row exists yet for this - // integration — fall back to the legacy direct `Inbox` write. - if (!integration.inboxId) { - // Workspace-level integration (no inbox) — nothing to disconnect. + // integration — fall back to the legacy direct `Inbox` write. Only for + // a genuine revocation; a transient failure with no `Connection` row + // to degrade must not disconnect the channel outright. + if (!(isRevoked && integration.inboxId)) { return } await db diff --git a/packages/connections/__tests__/registry.test.ts b/packages/connections/__tests__/registry.test.ts index 51c636db44..8953d3f3ea 100644 --- a/packages/connections/__tests__/registry.test.ts +++ b/packages/connections/__tests__/registry.test.ts @@ -76,4 +76,46 @@ describe("CONNECTION_REGISTRY", () => { expect(Boolean(adapter.credentialType), channel).toBe(requiresCredential) } }) + + it("credential-strategy providers either implement fromCredentials or are pinned as a known live-connect gap (T4)", () => { + // `resolve-provider.ts#resolveUnavailableReason` reports these as + // `notImplemented` in `GET /v1/connection-providers` specifically + // because they have no `fromCredentials`. Two distinct reasons: + // - `api`/`smtp`/`webchat`: self_serve with no external account to + // validate AND no stable per-instance identity to derive + // `Connection.sourceId` from before their satellite row exists (I1). + // - `chatbotx`: the internal built-in channel is auto-provisioned per + // workspace, never connected through a user-supplied credential. + // A new credential-strategy provider added here without `fromCredentials` + // silently becomes unreachable via `POST /v1/connections` — this pins + // the exception list so that is a conscious choice, not a silent gap. + const KNOWN_GAPS = new Set(["api", "chatbotx", "smtp", "webchat"]) + const CREDENTIAL_STRATEGIES = new Set(["token", "api_key", "self_serve"]) + const actualGaps: string[] = [] + for (const [type, adapter] of Object.entries(CONNECTION_REGISTRY)) { + if (!(adapter && CREDENTIAL_STRATEGIES.has(adapter.provider.strategy))) { + continue + } + if (!adapter.provider.fromCredentials) { + actualGaps.push(type) + } + } + expect(actualGaps.sort()).toEqual([...KNOWN_GAPS].sort()) + }) + + it("telegram (multiAccount + fromCredentials) derives sourceId from the bot token, never a constant (I1)", () => { + const adapter = CONNECTION_REGISTRY.telegram + expect(adapter?.provider.multiAccount).toBe(true) + expect(adapter?.provider.fromCredentials).toBeDefined() + const first = adapter?.provider.describe({ + authType: "secretText", + secretText: "111111:secret-a", + } as never) + const second = adapter?.provider.describe({ + authType: "secretText", + secretText: "222222:secret-b", + } as never) + expect(first?.sourceId).not.toBe("workspace") + expect(first?.sourceId).not.toBe(second?.sourceId) + }) }) diff --git a/packages/connections/__tests__/service.test.ts b/packages/connections/__tests__/service.test.ts index 3b1dbae805..0d5af461e9 100644 --- a/packages/connections/__tests__/service.test.ts +++ b/packages/connections/__tests__/service.test.ts @@ -1,4 +1,5 @@ import { channelLimitReachedException } from "@chatbotx.io/business/errors" +import { AuthException } from "@chatbotx.io/sdk" import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" const mocks = vi.hoisted(() => ({ @@ -373,11 +374,12 @@ describe("ConnectionService.disconnect", () => { expect(mocks.unsubscribe).toHaveBeenCalledWith({ auth: { authType: "none" }, }) - expect(mocks.deleteRowByForeignKey).toHaveBeenCalledWith("inbox-1") + expect(mocks.deleteRowByForeignKey).toHaveBeenCalledWith("inbox-1", "tx") expect(mocks.transition).toHaveBeenCalledWith({ connectionId: "conn-1", event: "user.disconnect", ownerId: "owner-1", + tx: "tx", }) expect(result.status).toBe("disconnected") }) @@ -399,19 +401,24 @@ describe("ConnectionService.disconnect", () => { connectionId: "conn-1", event: "user.disconnect", ownerId: undefined, + tx: "tx", }) }) - it("proceeds to the state transition even when provider-side teardown throws", async () => { + it("proceeds to the state transition and records lastError even when provider-side teardown throws", async () => { mocks.findByIdForWorkspace.mockResolvedValue(baseConnection()) mocks.disconnect.mockRejectedValue(new Error("upstream 500")) await connectionService.disconnect({ connectionId: "conn-1", workspaceId: "ws-1", }) - expect(mocks.deleteRowByForeignKey).toHaveBeenCalledWith("inbox-1") + expect(mocks.deleteRowByForeignKey).toHaveBeenCalledWith("inbox-1", "tx") + expect(mocks.update).toHaveBeenCalledWith( + { id: "conn-1", values: { lastError: "upstream 500" } }, + "tx", + ) expect(mocks.transition).toHaveBeenCalledWith( - expect.objectContaining({ event: "user.disconnect" }), + expect.objectContaining({ event: "user.disconnect", tx: "tx" }), ) }) }) @@ -476,6 +483,44 @@ describe("ConnectionService.refresh", () => { authExpiresAt: new Date("2030-01-01T00:00:00.000Z"), }) }) + + it("authStore.markOffline calls markUnhealthy for an AuthException", async () => { + mocks.findByIdForWorkspace.mockResolvedValue(baseConnection()) + mocks.findById.mockResolvedValue(baseConnection()) + mocks.ensureFreshAuth.mockImplementation(async (ctx) => { + await ctx.authStore.markOffline(new AuthException("revoked")) + }) + await connectionService.refresh({ + connectionId: "conn-1", + workspaceId: "ws-1", + }) + expect(mocks.markUnhealthy).toHaveBeenCalledWith({ + connectionId: "conn-1", + ownerId: "owner-1", + }) + expect(mocks.transition).not.toHaveBeenCalledWith( + expect.objectContaining({ event: "refresh.transient_failure" }), + ) + }) + + it("authStore.markOffline degrades via refresh.transient_failure for a non-auth error", async () => { + mocks.findByIdForWorkspace.mockResolvedValue(baseConnection()) + mocks.findById.mockResolvedValue(baseConnection()) + mocks.ensureFreshAuth.mockImplementation(async (ctx) => { + await ctx.authStore.markOffline(new Error("ECONNRESET")) + }) + await connectionService.refresh({ + connectionId: "conn-1", + workspaceId: "ws-1", + }) + expect(mocks.markUnhealthy).not.toHaveBeenCalled() + expect(mocks.transition).toHaveBeenCalledWith({ + connectionId: "conn-1", + event: "refresh.transient_failure", + reason: "refresh_failed", + ownerId: "owner-1", + }) + }) }) describe("ConnectionService.verify", () => { @@ -887,7 +932,7 @@ describe("ConnectionService.completeAuthorization", () => { ).rejects.toMatchObject({ code: "connectSessionExpired" }) }) - it("fails the session with provider_denied when exchangeCode throws", async () => { + it("fails the session with exchange_failed when exchangeCode throws", async () => { mocks.exchangeCode.mockRejectedValue(new Error("bad code")) await expect( connectionService.completeAuthorization({ @@ -900,7 +945,7 @@ describe("ConnectionService.completeAuthorization", () => { ).rejects.toMatchObject({ code: "connectionCredentialsRejected" }) expect(mocks.failSession).toHaveBeenCalledWith({ id: "session-1", - errorCode: "provider_denied", + errorCode: "exchange_failed", }) }) @@ -1414,4 +1459,22 @@ describe("ConnectionService.connectTargets", () => { mockAdapter.store.onDisconnect = "delete_row" }) + + it("sets Connection.createdBy to null for a token-initiated connect (no actorUserId) (T7)", async () => { + await connectionService.connectTargets({ + sessionId: "session-1", + workspaceId: "ws-1", + targetIds: ["page-1"], + // No `actorUserId` — the shape a public-API-token-authenticated + // `POST /v1/connect-sessions/{id}/targets` call always uses (the + // route never resolves/forwards one), matching `ConnectSession`'s + // own "token-actor session's Connection rows are created with + // createdBy = null" convention. + }) + + expect(mocks.insert).toHaveBeenCalledWith( + expect.objectContaining({ createdBy: null }), + "tx", + ) + }) }) diff --git a/packages/connections/src/connect-session-flow.ts b/packages/connections/src/connect-session-flow.ts new file mode 100644 index 0000000000..8a1322e910 --- /dev/null +++ b/packages/connections/src/connect-session-flow.ts @@ -0,0 +1,581 @@ +import { inboxService } from "@chatbotx.io/business" +import { connectSessionService } from "@chatbotx.io/business/connect-session" +import { + connectionStateService, + isActiveConnectionStatus, +} from "@chatbotx.io/business/connection" +import { + ChatbotXException, + connectionAlreadyConnectedException, + connectionCredentialsRejectedException, + connectionIdentityMismatchException, + connectionNoCandidatesException, + connectionNotConfiguredException, + connectionNotOAuthException, + connectionStateMismatchException, + connectSessionExpiredException, + notFoundException, + toPublicErrorMessage, +} from "@chatbotx.io/business/errors" +import { db } from "@chatbotx.io/database/client" +import type { + ChannelType, + ConnectSessionOutcome, + ConnectSessionPurpose, + IntegrationType, +} from "@chatbotx.io/database/partials" +import { connectionRepository } from "@chatbotx.io/database/repositories" +import type { + ConnectionModel, + ConnectSessionModel, +} from "@chatbotx.io/database/types" +import { encryptUtils } from "@chatbotx.io/encryption" +import type { + AuthValue, + ConnectionCandidate, + ConnectionCredential, + ConnectNextAction, +} from "@chatbotx.io/sdk" +import { + encryptedCandidatesSchema, + resolveAdapter, + resolveForeignKey, + resolveOwnerId, + subscribeWebhookBestEffort, + toChannelType, + upsertConnectionRow, +} from "./internal" +import { logger } from "./logger" + +/** + * `oauth_redirect`/`oauth_popup` connect: creates a `ConnectSession`, then + * builds the provider's `authorizeUrl` with `state = "{sessionId}.{nonce}"` + * — the OAuth callback hub resolves the session from that `state` alone + * (`ConnectSessionService.findByNonce`), before it has any other request + * context. `credential`/`callbackUrl` are resolved by the app-layer caller + * (tenant-aware platform credential + broker/custom-domain callback URL) + * and passed in — this package cannot resolve them itself without + * depending on `apps/builder`. + */ +export const startSession = async (input: { + workspaceId: string + provider: IntegrationType + purpose: ConnectSessionPurpose + credential: ConnectionCredential + callbackUrl: string + targetConnectionId?: string | null + actorUserId?: string | null + actorTokenId?: string | null + platformOwnerId?: string | null + originHost?: string | null + returnUrl?: string | null +}): Promise<{ + session: ConnectSessionModel + nextAction: ConnectNextAction +}> => { + const adapter = resolveAdapter(input.provider) + if (!adapter.provider.authorizeUrl) { + throw connectionNotOAuthException(input.provider) + } + + const { session, nonce } = await connectSessionService.create({ + workspaceId: input.workspaceId, + provider: input.provider, + purpose: input.purpose, + targetConnectionId: input.targetConnectionId, + actorUserId: input.actorUserId, + actorTokenId: input.actorTokenId, + platformOwnerId: input.platformOwnerId, + originHost: input.originHost, + returnUrl: input.returnUrl, + }) + + const url = adapter.provider.authorizeUrl({ + credential: input.credential, + callbackUrl: input.callbackUrl, + state: `${session.id}.${nonce}`, + }) + const nextAction: ConnectNextAction = { type: "open_url", url } + const updated = await connectSessionService.submitInput({ + id: session.id, + nextAction, + }) + return { session: updated, nextAction } +} + +/** + * OAuth callback exchange: resolves the session by its `state` nonce, + * exchanges `code` for `auth`, lists connectable candidates, and persists + * them as session targets — `attachAuthorization` always lands on + * `awaiting_selection`; auto-completing a single-target/non-multi-account + * provider is the caller's job (it has the `ConnectionProvider` in scope + * to check `multiAccount` and can immediately follow with + * `connectTargets`). + */ +export const completeAuthorization = async (input: { + sessionId: string + nonce: string + code: string + callbackUrl: string + credential: ConnectionCredential +}): Promise => { + const session = await connectSessionService.findByNonce(input.nonce) + if (!session || session.id !== input.sessionId) { + throw connectionStateMismatchException() + } + if (session.status !== "pending") { + throw connectSessionExpiredException( + "This connect session is no longer active.", + ) + } + // No local atomic claim on the session before `exchangeCode` (unlike + // `claimTarget`'s DB-level compare-and-set) — two concurrent calls for + // the same session could both pass this `status === "pending"` check + // and both call `exchangeCode` with the same `code`. Relies on the + // OAuth provider enforcing single-use authorization codes, which every + // provider this connects to does; a genuinely idempotent version would + // need its own compare-and-set (e.g. `pending` -> `authorizing`) here. + + const adapter = resolveAdapter(session.provider) + if (!adapter.provider.exchangeCode) { + throw connectionNotOAuthException(session.provider) + } + + let auth: AuthValue + try { + auth = await adapter.provider.exchangeCode({ + code: input.code, + callbackUrl: input.callbackUrl, + credential: input.credential, + }) + } catch (err) { + // The genuine "user clicked cancel" case is filtered upstream — the + // OAuth callback handler checks `?error=...` and marks the session + // `provider_denied` BEFORE ever calling `completeAuthorization` with + // a `code` (see `apps/builder/src/app/integrations/[...integration]/callback.ts`). + // Every failure that reaches this catch is therefore the token + // exchange itself failing — network error, malformed response, or the + // provider rejecting an invalid/expired code — never an explicit + // denial, so it gets its own distinct terminal code rather than + // reusing `provider_denied`. + await connectSessionService.fail({ + id: session.id, + errorCode: "exchange_failed", + }) + throw connectionCredentialsRejectedException( + toPublicErrorMessage(err, "The provider rejected the authorization."), + ) + } + + if (session.purpose === "reconnect" && session.targetConnectionId) { + return await completeReconnect({ session, auth }) + } + + return await listAndAttachCandidates(session, auth) +} + +/** + * Lists connectable candidates for an already-obtained `auth` and + * persists them as the session's `awaiting_selection` targets. Split out + * of `completeAuthorization` so a provider whose credential can be + * satisfied without a fresh OAuth round trip — Messenger's Facebook-SSO + * token reuse (`tryReuseFacebookSsoToken`), which skips the OAuth dialog + * entirely when the user's existing Facebook login already carries every + * required scope — can reach `awaiting_selection` directly from an + * app-layer-constructed `auth`, without a `code`/`nonce` to exchange. + */ +export const listAndAttachCandidates = async ( + session: ConnectSessionModel, + auth: AuthValue, +): Promise => { + const adapter = resolveAdapter(session.provider) + + let candidates: Awaited< + ReturnType> + > + try { + candidates = adapter.provider.listCandidates + ? await adapter.provider.listCandidates({ auth }) + : [{ ...adapter.provider.describe(auth), auth }] + } catch (err) { + await connectSessionService.fail({ + id: session.id, + errorCode: "provider_error", + }) + throw new Error(toPublicErrorMessage(err, "Failed to list accounts.")) + } + + if (candidates.length === 0) { + await connectSessionService.fail({ + id: session.id, + errorCode: "no_candidates", + }) + throw connectionNoCandidatesException() + } + + const targets = await Promise.all( + candidates.map(async (candidate) => { + if (candidate.alreadyConnected) { + return { + id: candidate.sourceId, + name: candidate.displayName, + avatarUrl: candidate.avatarUrl, + selectable: false, + alreadyConnected: candidate.alreadyConnected, + } + } + const existing = + await connectionRepository.findByProviderAndSourceIdAnyWorkspace({ + provider: session.provider, + sourceId: candidate.sourceId, + }) + if (existing && isActiveConnectionStatus(existing.status)) { + const scope: "this_workspace" | "other_workspace" = + existing.workspaceId === session.workspaceId + ? "this_workspace" + : "other_workspace" + return { + id: candidate.sourceId, + name: candidate.displayName, + avatarUrl: candidate.avatarUrl, + selectable: false, + alreadyConnected: scope, + } + } + return { + id: candidate.sourceId, + name: candidate.displayName, + avatarUrl: candidate.avatarUrl, + selectable: true, + } + }), + ) + + // Encrypts the full candidate list — not just the exchanged `auth` — so + // each candidate's own distinct `auth` (a multi-account provider's + // per-page token, e.g. Messenger) survives to `connectTargets`. For a + // single-target/`describe()`-fallback provider this is a one-element + // array holding the same `auth` `exchangeCode` returned. AAD binds the + // ciphertext to this exact session so it cannot be replayed against + // another session's row. + const encryptedAuth = await encryptUtils.encryptObject( + candidates, + `connect-session:${session.id}`, + ) + return await connectSessionService.attachAuthorization({ + id: session.id, + encryptedAuth, + targets, + }) +} + +/** + * `completeAuthorization`'s reconnect path: verifies the freshly + * re-authorized identity (`provider.describe(auth).sourceId`) matches the + * target `Connection`'s own `sourceId` — a user can grant access to a + * DIFFERENT account than the one being reconnected, which must not + * silently overwrite the wrong connection's auth — then saves the new + * auth and transitions the connection back to healthy. + */ +const completeReconnect = async (input: { + session: ConnectSessionModel + auth: AuthValue +}): Promise => { + const { session, auth } = input + const targetConnectionId = session.targetConnectionId + if (!targetConnectionId) { + throw notFoundException("Connection not found") + } + const connection = await connectionRepository.findById({ + id: targetConnectionId, + }) + if (!connection) { + await connectSessionService.fail({ + id: session.id, + errorCode: "internal_error", + }) + throw notFoundException("Connection not found") + } + + const adapter = resolveAdapter(connection.provider) + const descriptor = adapter.provider.describe(auth) + if (descriptor.sourceId !== connection.sourceId) { + await connectSessionService.fail({ + id: session.id, + errorCode: "provider_denied", + }) + throw connectionIdentityMismatchException() + } + + const foreignKey = resolveForeignKey(connection) + if (!(adapter.store && foreignKey)) { + await connectSessionService.fail({ + id: session.id, + errorCode: "internal_error", + }) + throw connectionNotConfiguredException(connection.provider) + } + const store = adapter.store + + const authExpiresAt = + auth.authType === "oauth2" && auth.tokens.expiresAt + ? new Date(auth.tokens.expiresAt) + : null + // `connect.completed`, not `auth.saved`/`recordAuthSaved` — `auth.saved` + // requires the connection to already be ACTIVE (`connected`/`degraded`) + // and throws otherwise (`state.ts`), but reconnect's whole purpose is + // reviving an INACTIVE (`needs_reauth`/`disconnected`) connection. + // `connect.completed` is the FSM event that actually allows that edge + // (and consumes quota on it for a channel-kind connection) — the same + // event `connectFromCredentials`'s revive path uses. + const ownerId = await resolveOwnerId(connection) + await db.transaction(async (tx) => { + await store.saveAuthByForeignKey(foreignKey, auth, tx) + await connectionRepository.update( + { id: connection.id, values: { authExpiresAt, lastError: null } }, + tx, + ) + await connectionStateService.transition({ + connectionId: connection.id, + event: "connect.completed", + ownerId, + tx, + }) + }) + + return await connectSessionService.recordResults({ + id: session.id, + results: [ + { + targetId: connection.sourceId, + status: "connected", + connectionId: connection.id, + }, + ], + resultConnectionIds: [connection.id], + }) +} + +/** + * Connects one already-authorized `ConnectionCandidate` — the per-target + * unit of work behind `connectTargets`. Channel-kind candidates mint (or + * revive) their `Inbox` row first via `inboxService.create`, since + * `Connection.inboxId` must exist before `store.insertRow` and before + * `connectionStateService.transition`'s own Inbox mirror can run; + * `skipQuota: true` there is required — `transition`'s quota edge is the + * sole quota consumption point on this path (see `credentials.ts + * #connectFromCredentials`'s revive-or-insert comment), so also consuming + * inside `inboxService.create` would charge a brand-new channel twice. + * Otherwise mirrors `connectFromCredentials`'s revive-or-insert + * transaction body via the shared `upsertConnectionRow`. + * + * Scope: a *bare* connect. The per-provider UI conveniences a + * picker-driven connect layers on top (Messenger's persistent-menu + * branding, workspace-logo push, tag-sync enqueue — see + * `apps/builder/src/features/integration-messenger/actions/connect-page.ts`) + * are NOT replicated here; `ConnectionProvider.actions` (reserved) is the + * intended future home for a client to opt into any of those separately. + */ +const connectCandidate = async (input: { + workspaceId: string + provider: IntegrationType + candidate: ConnectionCandidate + actorUserId?: string | null +}): Promise => { + const adapter = resolveAdapter(input.provider) + const { provider } = adapter + if (!adapter.store) { + throw connectionNotConfiguredException(input.provider) + } + const store = adapter.store + const auth = input.candidate.auth as AuthValue + const descriptor = provider.describe(auth) + const extraConfig = provider.candidateToConfig?.(auth) ?? {} + + const existing = await connectionRepository.findByProviderSourceId({ + workspaceId: input.workspaceId, + provider: input.provider, + sourceId: descriptor.sourceId, + }) + if (existing && isActiveConnectionStatus(existing.status)) { + throw connectionAlreadyConnectedException() + } + + const ownerId = await resolveOwnerId({ + kind: provider.kind, + workspaceId: input.workspaceId, + }) + + const connection = await db.transaction(async (tx) => { + let inboxId: string | undefined + if (provider.kind === "channel") { + if (!ownerId) { + throw notFoundException("Workspace owner not found") + } + const { inbox } = await inboxService.create({ + data: { + workspaceId: input.workspaceId, + channel: toChannelType(input.provider) as ChannelType, + sourceId: descriptor.sourceId, + name: descriptor.displayName, + }, + ownerId, + tx, + skipQuota: true, + }) + inboxId = inbox.id + } + + return await upsertConnectionRow({ + tx, + workspaceId: input.workspaceId, + provider: input.provider, + kind: provider.kind, + descriptor, + auth, + extraConfig, + existing, + store, + ownerId, + actorUserId: input.actorUserId, + inboxId, + }) + }) + + return await subscribeWebhookBestEffort({ + adapter, + auth, + connection, + ownerId, + }) +} + +/** + * Finishes an `awaiting_selection` connect session: atomically claims + * each requested target (safe against a double-submit or two tabs — a + * target claimed by a prior call maps to a `duplicated` outcome, never a + * second connect), then connects it via `connectCandidate`. Never throws + * for a single target's failure — every outcome (`connected`/ + * `duplicated`/`limitReached`/`failed`) is reported back per-target, the + * same vocabulary `CONNECT_ITEM_STATUSES` uses for the picker flows this + * replaces. + */ +export const connectTargets = async (input: { + sessionId: string + workspaceId: string + targetIds: string[] + actorUserId?: string | null +}): Promise<{ + session: ConnectSessionModel + connections: ConnectionModel[] + outcomes: ConnectSessionOutcome[] +}> => { + const session = await connectSessionService.findByIdForWorkspace({ + id: input.sessionId, + workspaceId: input.workspaceId, + }) + if (!session) { + throw notFoundException("Connect session not found") + } + if (session.status !== "awaiting_selection" || !session.encryptedAuth) { + throw connectSessionExpiredException( + "This connect session is not awaiting target selection.", + ) + } + + const candidates = await encryptUtils.decryptObject( + session.encryptedAuth, + encryptedCandidatesSchema, + `connect-session:${session.id}`, + ) + const candidateBySourceId = new Map( + candidates.map((candidate) => [candidate.sourceId, candidate]), + ) + + const outcomes: ConnectSessionOutcome[] = [] + const connections: ConnectionModel[] = [] + + for (const targetId of input.targetIds) { + const target = session.targets.find((t) => t.id === targetId) + const candidate = candidateBySourceId.get(targetId) + if (!(target && candidate)) { + outcomes.push({ targetId, status: "failed", reason: "unknown" }) + continue + } + if (!target.selectable) { + outcomes.push( + target.alreadyConnected + ? { targetId, status: "duplicated", reason: "alreadyConnected" } + : { targetId, status: "failed", reason: "notSelectable" }, + ) + continue + } + + const claimed = await connectSessionService.claimTarget({ + id: session.id, + targetId, + }) + if (!claimed) { + outcomes.push({ + targetId, + status: "duplicated", + reason: "alreadyConnected", + }) + continue + } + + try { + const connection = await connectCandidate({ + workspaceId: input.workspaceId, + provider: session.provider, + candidate: candidate as ConnectionCandidate, + actorUserId: input.actorUserId, + }) + connections.push(connection) + outcomes.push({ + targetId, + status: "connected", + connectionId: connection.id, + }) + } catch (err) { + if ( + err instanceof ChatbotXException && + err.code === "channelLimitReached" + ) { + outcomes.push({ + targetId, + status: "limitReached", + reason: "workspaceLimit", + }) + } else if ( + err instanceof ChatbotXException && + err.code === "connectionAlreadyConnected" + ) { + outcomes.push({ + targetId, + status: "duplicated", + reason: "alreadyConnected", + }) + } else { + logger.warn( + { err, targetId, provider: session.provider }, + "connectTargets: candidate connect failed", + ) + outcomes.push({ + targetId, + status: "failed", + reason: "providerRejected", + detail: toPublicErrorMessage(err, "Connect failed"), + }) + } + } + } + + const updatedSession = await connectSessionService.recordResults({ + id: session.id, + results: outcomes, + resultConnectionIds: connections.map((connection) => connection.id), + }) + + return { session: updatedSession, connections, outcomes } +} diff --git a/packages/connections/src/credentials.ts b/packages/connections/src/credentials.ts new file mode 100644 index 0000000000..4a3753a783 --- /dev/null +++ b/packages/connections/src/credentials.ts @@ -0,0 +1,177 @@ +import { isActiveConnectionStatus } from "@chatbotx.io/business/connection" +import { + connectionAlreadyConnectedException, + connectionCredentialsRejectedException, + connectionNotConfiguredException, + connectionWrongStrategyException, + toPublicErrorMessage, +} from "@chatbotx.io/business/errors" +import { db } from "@chatbotx.io/database/client" +import type { IntegrationType } from "@chatbotx.io/database/partials" +import { connectionRepository } from "@chatbotx.io/database/repositories" +import type { + ConnectionModel, + ConnectSessionModel, +} from "@chatbotx.io/database/types" +import type { + AuthValue, + ConnectionCredential, + ConnectNextAction, +} from "@chatbotx.io/sdk" +import { startSession } from "./connect-session-flow" +import { + findOrThrow, + parseConfig, + resolveAdapter, + resolveOwnerId, + subscribeWebhookBestEffort, + upsertConnectionRow, +} from "./internal" + +/** + * `token`/`api_key`/`self_serve` connect: validates `config` against the + * provider's `configFields`, live-validates it via `fromCredentials`, then + * creates (or revives a previously disconnected) `Connection` row plus its + * satellite table row in one transaction. Today every credential-strategy + * provider is `kind: "integration"` (a workspace singleton, no quota + * edge), but the transition still runs through `connectionStateService` + * — not a hardcoded `status: "connected"` insert — so a future `kind: + * "channel"` credential-strategy provider consumes quota correctly too. + */ +export const connectFromCredentials = async (input: { + workspaceId: string + provider: IntegrationType + config: Record + actorUserId?: string | null + /** + * Allows replacing an already-`connected`/`degraded` connection's auth + * and config in place instead of throwing `connectionAlreadyConnected` + * — only for backward-compat upsert aliases (the legacy `PUT + * /v1/integrations/ai/{provider}` route, which has always replaced the + * stored API key/config on repeat calls). The new `POST /v1/connections` + * surface must NOT set this — a fresh connect should reject an existing + * active connection. + */ + allowUpdate?: boolean +}): Promise => { + const adapter = resolveAdapter(input.provider) + const { provider } = adapter + const isCredentialStrategy = + provider.strategy === "token" || + provider.strategy === "api_key" || + provider.strategy === "self_serve" + if (!(isCredentialStrategy && provider.fromCredentials)) { + throw connectionWrongStrategyException(input.provider) + } + if (!adapter.store) { + throw connectionNotConfiguredException(input.provider) + } + const store = adapter.store + + const parsedConfig = parseConfig(provider.configFields, input.config) + // Fields the caller sent that aren't part of the provider's own + // credential shape (`configFields`) — e.g. an AI provider's `model`/ + // `temperature`/`maxOutputTokens` — flow straight through to the + // satellite row's extra columns via `store.insertRow`'s `config`, + // unvalidated (the satellite table's own NOT NULL/type constraints are + // the validation for those). + const configFieldNames = new Set( + provider.configFields.map((field) => field.name), + ) + const extraConfig = Object.fromEntries( + Object.entries(input.config).filter(([key]) => !configFieldNames.has(key)), + ) + + let auth: AuthValue + try { + auth = await provider.fromCredentials(parsedConfig) + } catch (err) { + throw connectionCredentialsRejectedException( + toPublicErrorMessage(err, "The provided credentials were rejected."), + ) + } + + const descriptor = provider.describe(auth) + + const existing = await connectionRepository.findByProviderSourceId({ + workspaceId: input.workspaceId, + provider: input.provider, + sourceId: descriptor.sourceId, + }) + if ( + existing && + isActiveConnectionStatus(existing.status) && + !input.allowUpdate + ) { + throw connectionAlreadyConnectedException() + } + + const ownerId = await resolveOwnerId({ + kind: provider.kind, + workspaceId: input.workspaceId, + }) + + const connection = await db.transaction( + async (tx) => + await upsertConnectionRow({ + tx, + workspaceId: input.workspaceId, + provider: input.provider, + kind: provider.kind, + descriptor, + auth, + extraConfig, + existing, + store, + ownerId, + actorUserId: input.actorUserId, + }), + ) + + return await subscribeWebhookBestEffort({ + adapter, + auth, + connection, + ownerId, + }) +} + +/** + * Re-authorizes an existing (typically `needs_reauth`) `Connection` — a + * `startSession` with `purpose: "reconnect"` and `targetConnectionId` set, + * so `completeAuthorization` skips candidate selection entirely and + * verifies the re-granted account's identity matches this exact + * connection instead. + */ +export const reconnect = async (input: { + connectionId: string + workspaceId: string + credential: ConnectionCredential + callbackUrl: string + actorUserId?: string | null + actorTokenId?: string | null + platformOwnerId?: string | null + originHost?: string | null + returnUrl?: string | null +}): Promise<{ + session: ConnectSessionModel + nextAction: ConnectNextAction +}> => { + const connection = await findOrThrow({ + connectionId: input.connectionId, + workspaceId: input.workspaceId, + }) + return await startSession({ + workspaceId: input.workspaceId, + provider: connection.provider, + purpose: "reconnect", + credential: input.credential, + callbackUrl: input.callbackUrl, + targetConnectionId: connection.id, + actorUserId: input.actorUserId, + actorTokenId: input.actorTokenId, + platformOwnerId: input.platformOwnerId, + originHost: input.originHost, + returnUrl: input.returnUrl, + }) +} diff --git a/packages/connections/src/internal.ts b/packages/connections/src/internal.ts new file mode 100644 index 0000000000..10cf471251 --- /dev/null +++ b/packages/connections/src/internal.ts @@ -0,0 +1,336 @@ +import { workspaceMemberService } from "@chatbotx.io/business" +import { + type ConnectionAdapter, + connectionStateService, +} from "@chatbotx.io/business/connection" +import { + connectionAlreadyConnectedException, + connectionNotConfiguredException, + notFoundException, + validationException, +} from "@chatbotx.io/business/errors" +import { + type DatabaseClient, + isUniqueViolationError, +} from "@chatbotx.io/database/client" +import type { + ChannelType, + IntegrationType, +} from "@chatbotx.io/database/partials" +import { connectionRepository } from "@chatbotx.io/database/repositories" +import type { ConnectionModel } from "@chatbotx.io/database/types" +import type { + AuthValue, + ConnectionConfigField, + ConnectionDescriptor, + ConnectionKind, +} from "@chatbotx.io/sdk" +import { z } from "zod" +import { logger } from "./logger" +import { CONNECTION_REGISTRY } from "./registry" + +/** + * What a session's `encryptedAuth` blob actually holds once + * `completeAuthorization` lists candidates: the full `ConnectionCandidate[]` + * (each with its own `auth`), not just the single exchanged `auth` — a + * multi-account provider (Messenger) hands back one distinct per-page auth + * per candidate. `auth` itself is provider-specific and unvalidated here + * (same trust boundary as `asAuthValue` in `store-bindings.ts`); everything + * else mirrors `ConnectSessionTarget`. + */ +export const encryptedCandidatesSchema = z.array( + z.object({ + sourceId: z.string(), + displayName: z.string(), + authExpiresAt: z.string().optional(), + avatarUrl: z.string().optional(), + alreadyConnected: z.enum(["this_workspace", "other_workspace"]).optional(), + auth: z.unknown(), + }), +) + +/** + * The FK a `Connection` row actually carries to its satellite row — + * `inboxId` for channels, `integrationId` for workspace integrations. `null` + * only for `chatbotx` (no satellite table at all). + */ +export const resolveForeignKey = (connection: ConnectionModel): string | null => + connection.inboxId ?? connection.integrationId ?? null + +/** + * Validates a raw `config` object (a credential-strategy `connect` request + * body) against a provider's `configFields` declaration, coercing each + * value to its declared type. Throws the same `validationException` shape + * every other service uses, field-scoped, so a public/private API caller + * can attach it to the right form field without special-casing this path. + */ +export const parseConfig = ( + configFields: readonly ConnectionConfigField[], + rawConfig: Record, +): Record => { + const parsed: Record = {} + for (const field of configFields) { + const value = rawConfig[field.name] + if (value === undefined || value === null || value === "") { + if (field.required) { + throw validationException(field.name, `${field.name} is required`) + } + continue + } + switch (field.type) { + case "string": + case "secret": + case "url": + if (typeof value !== "string") { + throw validationException( + field.name, + `${field.name} must be a string`, + ) + } + parsed[field.name] = value + break + case "number": { + const num = typeof value === "number" ? value : Number(value) + if (Number.isNaN(num)) { + throw validationException( + field.name, + `${field.name} must be a number`, + ) + } + parsed[field.name] = num + break + } + case "boolean": + if (typeof value !== "boolean") { + throw validationException( + field.name, + `${field.name} must be a boolean`, + ) + } + parsed[field.name] = value + break + case "enum": + if (typeof value !== "string" || !field.enumValues?.includes(value)) { + throw validationException( + field.name, + `${field.name} must be one of ${(field.enumValues ?? []).join(", ")}`, + ) + } + parsed[field.name] = value + break + default: { + const exhaustive: never = field.type + throw new Error(`Unhandled connection config field type: ${exhaustive}`) + } + } + } + return parsed +} + +export const resolveAdapter = (provider: string): ConnectionAdapter => { + const adapter = + CONNECTION_REGISTRY[provider as keyof typeof CONNECTION_REGISTRY] + if (!adapter) { + throw connectionNotConfiguredException(provider) + } + return adapter +} + +export const findOrThrow = async (input: { + connectionId: string + workspaceId: string +}): Promise => { + const connection = await connectionRepository.findByIdForWorkspace({ + id: input.connectionId, + workspaceId: input.workspaceId, + }) + if (!connection) { + throw notFoundException("Connection not found") + } + return connection +} + +/** + * Best-effort `provider.webhook.subscribe` right after a fresh + * `connect.completed` — the FSM design's own "webhook.subscribe + * best-effort (failure → degraded, lastError)" edge (`state.ts`'s + * `verify.failed_non_auth`), mirrored from `disconnect`'s existing + * best-effort `webhook.unsubscribe` call. Never throws: a subscribe + * failure degrades the just-created connection instead of failing the + * whole connect (the row and quota consumption already committed). + */ +export const subscribeWebhookBestEffort = async (input: { + adapter: ConnectionAdapter + auth: AuthValue + connection: ConnectionModel + ownerId: string | undefined +}): Promise => { + if (!input.adapter.provider.webhook) { + return input.connection + } + try { + await input.adapter.provider.webhook.subscribe({ auth: input.auth }) + return input.connection + } catch (err) { + logger.warn( + { + err, + connectionId: input.connection.id, + provider: input.connection.provider, + }, + "connect: webhook subscribe failed, marking connection degraded", + ) + return await connectionStateService.transition({ + connectionId: input.connection.id, + event: "verify.failed_non_auth", + reason: "verify_failed", + ownerId: input.ownerId, + }) + } +} + +/** + * Resolves the workspace-owner user id for the FSM's quota edge — + * `undefined` for anything but a `kind: "channel"` connection, since + * `ConnectionStateService.transition`'s quota edge always targets the + * `"channels"` metric. Passing an owner for a workspace-integration + * connection (AI providers, marketing tools) would incorrectly + * consume/release a channel-quota slot when that row crosses the + * active/inactive boundary. + */ +export const resolveOwnerId = async ( + connection: Pick, +): Promise => { + if (connection.kind !== "channel") { + return + } + return await workspaceMemberService.findOwnerUserIdByWorkspaceId({ + workspaceId: connection.workspaceId, + }) +} + +/** + * `instagramFacebook`'s satellite table is shared with `instagram` + * (`IntegrationInstagram`, disambiguated by its `type` column — see + * `CONNECTION_STORE_BINDINGS`), but `Inbox.channel` has no matching + * `instagramFacebook` value (`ChannelType` only has `instagram`) — every + * other channel-kind `IntegrationType` literal is already a valid + * `ChannelType`. Only called for `kind === "channel"` providers. + */ +export const toChannelType = (provider: IntegrationType): ChannelType => + provider === "instagramFacebook" ? "instagram" : (provider as ChannelType) + +/** + * Revive-or-insert-then-transition: the ~90-line block `connectFromCredentials` + * and `connectCandidate` each ran independently before this extraction. + * Revives a `keep_row` provider's satellite row in place (its unique + * constraint would otherwise collide with a fresh `insertRow`), otherwise + * inserts a new satellite row (mapping a duplicate-constraint violation to + * `connectionAlreadyConnectedException`); then updates or inserts the + * `Connection` row and drives it through `connect.completed` — the sole + * event `ConnectionStateService.transition` consumes quota from, so this is + * inserted `disconnected` and transitioned, never hardcoded `connected`. + */ +export const upsertConnectionRow = async (input: { + tx: DatabaseClient + workspaceId: string + provider: IntegrationType + kind: ConnectionKind + descriptor: ConnectionDescriptor + auth: AuthValue + extraConfig: Record + existing: ConnectionModel | undefined + store: NonNullable + ownerId: string | undefined + actorUserId?: string | null + inboxId?: string | null +}): Promise => { + const { + tx, + workspaceId, + provider, + kind, + descriptor, + auth, + extraConfig, + existing, + store, + ownerId, + actorUserId, + inboxId, + } = input + + let integrationId: string | undefined + if (existing && store.onDisconnect === "keep_row") { + const existingForeignKey = resolveForeignKey(existing) + if (existingForeignKey) { + await store.saveAuthByForeignKey(existingForeignKey, auth, tx) + } + integrationId = existing.integrationId ?? undefined + } else { + try { + const inserted = await store.insertRow( + { + workspaceId, + inboxId: inboxId ?? undefined, + auth, + descriptor, + config: extraConfig, + }, + tx, + ) + integrationId = inserted.integrationId + } catch (err) { + if ( + store.duplicateConstraint && + isUniqueViolationError(err, store.duplicateConstraint) + ) { + throw connectionAlreadyConnectedException() + } + throw err + } + } + + if (existing) { + await connectionRepository.update( + { + id: existing.id, + values: { + inboxId: inboxId ?? existing.inboxId, + integrationId: integrationId ?? null, + displayName: descriptor.displayName, + lastError: null, + statusReason: null, + }, + }, + tx, + ) + return await connectionStateService.transition({ + connectionId: existing.id, + event: "connect.completed", + ownerId, + tx, + }) + } + + const created = await connectionRepository.insert( + { + workspaceId, + provider, + kind, + sourceId: descriptor.sourceId, + displayName: descriptor.displayName, + inboxId: inboxId ?? null, + integrationId: integrationId ?? null, + status: "disconnected", + createdBy: actorUserId ?? null, + }, + tx, + ) + return await connectionStateService.transition({ + connectionId: created.id, + event: "connect.completed", + ownerId, + tx, + }) +} diff --git a/packages/connections/src/lifecycle.ts b/packages/connections/src/lifecycle.ts new file mode 100644 index 0000000000..e8fbbd9058 --- /dev/null +++ b/packages/connections/src/lifecycle.ts @@ -0,0 +1,220 @@ +import { + connectionStateService, + isActiveConnectionStatus, +} from "@chatbotx.io/business/connection" +import { + connectionInactiveException, + connectionNotConfiguredException, + connectionNotRefreshableException, + notFoundException, + toPublicErrorMessage, +} from "@chatbotx.io/business/errors" +import { db } from "@chatbotx.io/database/client" +import { connectionRepository } from "@chatbotx.io/database/repositories" +import type { ConnectionModel } from "@chatbotx.io/database/types" +import type { AuthStore, AuthValue } from "@chatbotx.io/sdk" +import { AuthException } from "@chatbotx.io/sdk" +import { + findOrThrow, + resolveAdapter, + resolveForeignKey, + resolveOwnerId, +} from "./internal" +import { logger } from "./logger" + +/** + * User-initiated teardown: best-effort provider-side disconnect + webhook + * unsubscribe (never blocks the local state transition on an upstream + * failure), then the satellite row's own `onDisconnect` policy + * (`delete_row`/`keep_row`), then the FSM transition. + * + * Scope note: this is the **generic** disconnect path shared by every + * provider. Bespoke per-provider teardown side effects that predate the + * Connection domain — messenger's shared-IG-page `general_info` + * preservation, WhatsApp's coexist/staging cleanup, TikTok/Zalo specifics — + * are NOT ported here; those remain in their existing per-channel disconnect + * actions until a dedicated follow-up audits each one individually. + */ +export const disconnect = async (input: { + connectionId: string + workspaceId: string +}): Promise => { + const connection = await findOrThrow(input) + const adapter = resolveAdapter(connection.provider) + const foreignKey = resolveForeignKey(connection) + let teardownError: string | null = null + + if (adapter.store && foreignKey) { + try { + const auth = await adapter.store.loadAuthByForeignKey(foreignKey) + if (adapter.integration) { + await adapter.integration.disconnect(auth) + } + if (adapter.provider.webhook) { + await adapter.provider.webhook.unsubscribe({ auth }) + } + } catch (err) { + // Surfaced onto the row (not just logged) — a webhook left + // subscribed or a provider-side disconnect that silently failed is + // otherwise indistinguishable from a clean teardown once the row + // flips to `disconnected`. + teardownError = toPublicErrorMessage(err, "Provider-side teardown failed") + logger.warn( + { err, connectionId: connection.id, provider: connection.provider }, + "connection disconnect: provider-side teardown failed, proceeding with local disconnect", + ) + } + } + + const ownerId = await resolveOwnerId(connection) + // `deleteRowByForeignKey` and the FSM transition share one transaction: + // if the satellite delete throws, the whole disconnect rolls back + // instead of leaving the Connection row `connected` with its satellite + // row already gone. + return await db.transaction(async (tx) => { + if (adapter.store && foreignKey) { + await adapter.store.deleteRowByForeignKey(foreignKey, tx) + } + if (teardownError) { + await connectionRepository.update( + { id: connection.id, values: { lastError: teardownError } }, + tx, + ) + } + return await connectionStateService.transition({ + connectionId: connection.id, + event: "user.disconnect", + ownerId, + tx, + }) + }) +} + +/** Forces `refreshAuth` regardless of expiry — `POST /v1/connections/{id}/refresh`. */ +export const refresh = async (input: { + connectionId: string + workspaceId: string +}): Promise => { + const connection = await findOrThrow(input) + if (!isActiveConnectionStatus(connection.status)) { + throw connectionInactiveException() + } + const adapter = resolveAdapter(connection.provider) + if (!adapter.integration?.refreshAuth) { + throw connectionNotRefreshableException(connection.provider) + } + if (!adapter.store) { + throw connectionNotConfiguredException(connection.provider) + } + const foreignKey = resolveForeignKey(connection) + if (!foreignKey) { + throw connectionNotConfiguredException(connection.provider) + } + const store = adapter.store + + const auth = await store.loadAuthByForeignKey(foreignKey) + const authStore: AuthStore = { + load: async () => await store.loadAuthByForeignKey(foreignKey), + save: async (newAuth) => { + await store.saveAuthByForeignKey(foreignKey, newAuth) + const authExpiresAt = + newAuth.authType === "oauth2" && newAuth.tokens.expiresAt + ? new Date(newAuth.tokens.expiresAt) + : null + await connectionStateService.recordAuthSaved({ + connectionId: connection.id, + authExpiresAt, + }) + }, + markOffline: async (reason?: unknown) => { + const ownerId = await resolveOwnerId(connection) + if (reason instanceof AuthException) { + await connectionStateService.markUnhealthy({ + connectionId: connection.id, + ownerId, + }) + return + } + try { + await connectionStateService.transition({ + connectionId: connection.id, + event: "refresh.transient_failure", + reason: "refresh_failed", + ownerId, + }) + } catch { + // Not currently active — nothing to degrade. + } + }, + } + + // `refreshAuth`/`ensureFreshAuth` only ever read `ctx.auth`/`ctx.authStore` + // (never `ctx.platform`/`ctx.storagePrefix`/`ctx.integrationDetail`) — + // see `Integration.refreshAndPersist` in `@chatbotx.io/sdk`. The + // `platform` stub below is structurally required but never invoked on + // this path. + await adapter.integration.ensureFreshAuth( + { + storagePrefix: "", + auth, + authStore, + platform: { + appUrl: "", + wsUrl: "", + storageUrl: "", + getRealtimeAuthHeaders: async () => ({}), + }, + }, + { force: true }, + ) + + const refreshed = await connectionRepository.findById({ id: connection.id }) + if (!refreshed) { + throw notFoundException("Connection not found") + } + return refreshed +} + +/** Live health check without a refresh cycle — `POST /v1/connections/{id}/verify`. */ +export const verify = async (input: { + connectionId: string + workspaceId: string +}): Promise => { + const connection = await findOrThrow(input) + if (!isActiveConnectionStatus(connection.status)) { + throw connectionInactiveException() + } + const adapter = resolveAdapter(connection.provider) + if (!adapter.store) { + throw connectionNotConfiguredException(connection.provider) + } + const foreignKey = resolveForeignKey(connection) + if (!foreignKey) { + throw connectionNotConfiguredException(connection.provider) + } + + const auth = await adapter.store.loadAuthByForeignKey(foreignKey) + const health = await adapter.provider.verify({ auth }) + const ownerId = await resolveOwnerId(connection) + + if (health.ok) { + return await connectionStateService.transition({ + connectionId: connection.id, + event: "verify.ok", + ownerId, + }) + } + if (health.revoked) { + return await connectionStateService.markUnhealthy({ + connectionId: connection.id, + reason: "token_revoked", + ownerId, + }) + } + return await connectionStateService.transition({ + connectionId: connection.id, + event: "verify.failed_non_auth", + reason: "verify_failed", + ownerId, + }) +} diff --git a/packages/connections/src/service.ts b/packages/connections/src/service.ts index 30992f9b8a..abc1800410 100644 --- a/packages/connections/src/service.ts +++ b/packages/connections/src/service.ts @@ -1,1224 +1,42 @@ -import { inboxService, workspaceMemberService } from "@chatbotx.io/business" -import { connectSessionService } from "@chatbotx.io/business/connect-session" import { - type ConnectionAdapter, - connectionStateService, - isActiveConnectionStatus, -} from "@chatbotx.io/business/connection" -import { - ChatbotXException, - connectionAlreadyConnectedException, - connectionCredentialsRejectedException, - connectionIdentityMismatchException, - connectionInactiveException, - connectionNoCandidatesException, - connectionNotConfiguredException, - connectionNotOAuthException, - connectionNotRefreshableException, - connectionStateMismatchException, - connectionWrongStrategyException, - connectSessionExpiredException, - notFoundException, - toPublicErrorMessage, - validationException, -} from "@chatbotx.io/business/errors" -import { db, isUniqueViolationError } from "@chatbotx.io/database/client" -import type { - ChannelType, - ConnectSessionOutcome, - ConnectSessionPurpose, - IntegrationType, -} from "@chatbotx.io/database/partials" -import { connectionRepository } from "@chatbotx.io/database/repositories" -import type { - ConnectionModel, - ConnectSessionModel, -} from "@chatbotx.io/database/types" -import { encryptUtils } from "@chatbotx.io/encryption" -import type { - AuthStore, - AuthValue, - ConnectionCandidate, - ConnectionConfigField, - ConnectionCredential, - ConnectNextAction, -} from "@chatbotx.io/sdk" -import { z } from "zod" -import { logger } from "./logger" -import { CONNECTION_REGISTRY } from "./registry" - -/** - * What a session's `encryptedAuth` blob actually holds once - * `completeAuthorization` lists candidates: the full `ConnectionCandidate[]` - * (each with its own `auth`), not just the single exchanged `auth` — a - * multi-account provider (Messenger) hands back one distinct per-page auth - * per candidate. `auth` itself is provider-specific and unvalidated here - * (same trust boundary as `asAuthValue` in `store-bindings.ts`); everything - * else mirrors `ConnectSessionTarget`. - */ -const encryptedCandidatesSchema = z.array( - z.object({ - sourceId: z.string(), - displayName: z.string(), - authExpiresAt: z.string().optional(), - avatarUrl: z.string().optional(), - alreadyConnected: z.enum(["this_workspace", "other_workspace"]).optional(), - auth: z.unknown(), - }), -) - -/** - * The FK a `Connection` row actually carries to its satellite row — - * `inboxId` for channels, `integrationId` for workspace integrations. `null` - * only for `chatbotx` (no satellite table at all). - */ -const resolveForeignKey = (connection: ConnectionModel): string | null => - connection.inboxId ?? connection.integrationId ?? null - -/** - * Validates a raw `config` object (a credential-strategy `connect` request - * body) against a provider's `configFields` declaration, coercing each - * value to its declared type. Throws the same `validationException` shape - * every other service uses, field-scoped, so a public/private API caller - * can attach it to the right form field without special-casing this path. - */ -const parseConfig = ( - configFields: readonly ConnectionConfigField[], - rawConfig: Record, -): Record => { - const parsed: Record = {} - for (const field of configFields) { - const value = rawConfig[field.name] - if (value === undefined || value === null || value === "") { - if (field.required) { - throw validationException(field.name, `${field.name} is required`) - } - continue - } - switch (field.type) { - case "string": - case "secret": - case "url": - if (typeof value !== "string") { - throw validationException( - field.name, - `${field.name} must be a string`, - ) - } - parsed[field.name] = value - break - case "number": { - const num = typeof value === "number" ? value : Number(value) - if (Number.isNaN(num)) { - throw validationException( - field.name, - `${field.name} must be a number`, - ) - } - parsed[field.name] = num - break - } - case "boolean": - if (typeof value !== "boolean") { - throw validationException( - field.name, - `${field.name} must be a boolean`, - ) - } - parsed[field.name] = value - break - case "enum": - if (typeof value !== "string" || !field.enumValues?.includes(value)) { - throw validationException( - field.name, - `${field.name} must be one of ${(field.enumValues ?? []).join(", ")}`, - ) - } - parsed[field.name] = value - break - default: { - const exhaustive: never = field.type - throw new Error(`Unhandled connection config field type: ${exhaustive}`) - } - } - } - return parsed -} + completeAuthorization, + connectTargets, + listAndAttachCandidates, + startSession, +} from "./connect-session-flow" +import { connectFromCredentials, reconnect } from "./credentials" +import { disconnect, refresh, verify } from "./lifecycle" /** * Registry-aware orchestration over the `Connection` domain: provider-side - * teardown/refresh/health-check calls plus the store-binding CRUD, delegating - * every status write to the registry-free `connectionStateService`. Lives in - * `@chatbotx.io/connections` (not `@chatbotx.io/business`) because it needs - * the full `CONNECTION_REGISTRY`, which itself depends on `business` — see - * the placement note on `connectionStateService`. + * connect/disconnect/verify/refresh, `ConnectSession`-based OAuth flows, and + * credential-strategy connects. Split across sibling modules to stay under + * the project's 800-line cap per file: + * + * - `internal.ts` — shared private helpers (`resolveAdapter`, + * `findOrThrow`, `resolveOwnerId`, `subscribeWebhookBestEffort`, + * `toChannelType`, `upsertConnectionRow`) plus `parseConfig`/ + * `resolveForeignKey`/`encryptedCandidatesSchema`. + * - `lifecycle.ts` — `disconnect`/`refresh`/`verify`. + * - `credentials.ts` — `connectFromCredentials`/`reconnect`. + * - `connect-session-flow.ts` — `startSession`/`completeAuthorization`/ + * `listAndAttachCandidates`/`connectTargets` plus the session-flow-only + * private helpers `completeReconnect`/`connectCandidate`. + * + * This class is the one public surface (`connectionService`) every caller + * imports; the split above is purely a file-organization concern; behavior + * is unchanged. */ class ConnectionService { - private resolveAdapter(provider: string): ConnectionAdapter { - const adapter = - CONNECTION_REGISTRY[provider as keyof typeof CONNECTION_REGISTRY] - if (!adapter) { - throw connectionNotConfiguredException(provider) - } - return adapter - } - - private async findOrThrow(input: { - connectionId: string - workspaceId: string - }): Promise { - const connection = await connectionRepository.findByIdForWorkspace({ - id: input.connectionId, - workspaceId: input.workspaceId, - }) - if (!connection) { - throw notFoundException("Connection not found") - } - return connection - } - - /** - * Best-effort `provider.webhook.subscribe` right after a fresh - * `connect.completed` — the FSM design's own "webhook.subscribe - * best-effort (failure → degraded, lastError)" edge (`state.ts`'s - * `verify.failed_non_auth`), mirrored from `disconnect`'s existing - * best-effort `webhook.unsubscribe` call. Never throws: a subscribe - * failure degrades the just-created connection instead of failing the - * whole connect (the row and quota consumption already committed). - */ - private async subscribeWebhookBestEffort(input: { - adapter: ConnectionAdapter - auth: AuthValue - connection: ConnectionModel - ownerId: string | undefined - }): Promise { - if (!input.adapter.provider.webhook) { - return input.connection - } - try { - await input.adapter.provider.webhook.subscribe({ auth: input.auth }) - return input.connection - } catch (err) { - logger.warn( - { - err, - connectionId: input.connection.id, - provider: input.connection.provider, - }, - "connect: webhook subscribe failed, marking connection degraded", - ) - return await connectionStateService.transition({ - connectionId: input.connection.id, - event: "verify.failed_non_auth", - reason: "verify_failed", - ownerId: input.ownerId, - }) - } - } - - /** - * Resolves the workspace-owner user id for the FSM's quota edge — - * `undefined` for anything but a `kind: "channel"` connection, since - * `ConnectionStateService.applyQuotaEdge` always targets the `"channels"` - * metric. Passing an owner for a workspace-integration connection (AI - * providers, marketing tools) would incorrectly consume/release a - * channel-quota slot when that row crosses the active/inactive boundary. - */ - private async resolveOwnerId( - connection: Pick, - ): Promise { - if (connection.kind !== "channel") { - return - } - return await workspaceMemberService.findOwnerUserIdByWorkspaceId({ - workspaceId: connection.workspaceId, - }) - } - - /** - * User-initiated teardown: best-effort provider-side disconnect + webhook - * unsubscribe (never blocks the local state transition on an upstream - * failure), then the satellite row's own `onDisconnect` policy - * (`delete_row`/`keep_row`), then the FSM transition. - * - * Scope note: this is the **generic** disconnect path shared by every - * provider. Bespoke per-provider teardown side effects that predate the - * Connection domain — messenger's shared-IG-page `general_info` - * preservation, WhatsApp's coexist/staging cleanup, TikTok/Zalo specifics — - * are NOT ported here; those remain in their existing per-channel disconnect - * actions until a dedicated follow-up audits each one individually. - */ - async disconnect(input: { - connectionId: string - workspaceId: string - }): Promise { - const connection = await this.findOrThrow(input) - const adapter = this.resolveAdapter(connection.provider) - const foreignKey = resolveForeignKey(connection) - - if (adapter.store && foreignKey) { - try { - const auth = await adapter.store.loadAuthByForeignKey(foreignKey) - if (adapter.integration) { - await adapter.integration.disconnect(auth) - } - if (adapter.provider.webhook) { - await adapter.provider.webhook.unsubscribe({ auth }) - } - } catch (err) { - logger.warn( - { err, connectionId: connection.id, provider: connection.provider }, - "connection disconnect: provider-side teardown failed, proceeding with local disconnect", - ) - } - await adapter.store.deleteRowByForeignKey(foreignKey) - } - - const ownerId = await this.resolveOwnerId(connection) - return await connectionStateService.transition({ - connectionId: connection.id, - event: "user.disconnect", - ownerId, - }) - } - - /** Forces `refreshAuth` regardless of expiry — `POST /v1/connections/{id}/refresh`. */ - async refresh(input: { - connectionId: string - workspaceId: string - }): Promise { - const connection = await this.findOrThrow(input) - if (!isActiveConnectionStatus(connection.status)) { - throw connectionInactiveException() - } - const adapter = this.resolveAdapter(connection.provider) - if (!adapter.integration?.refreshAuth) { - throw connectionNotRefreshableException(connection.provider) - } - if (!adapter.store) { - throw connectionNotConfiguredException(connection.provider) - } - const foreignKey = resolveForeignKey(connection) - if (!foreignKey) { - throw connectionNotConfiguredException(connection.provider) - } - const store = adapter.store - - const auth = await store.loadAuthByForeignKey(foreignKey) - const authStore: AuthStore = { - load: async () => await store.loadAuthByForeignKey(foreignKey), - save: async (newAuth) => { - await store.saveAuthByForeignKey(foreignKey, newAuth) - const authExpiresAt = - newAuth.authType === "oauth2" && newAuth.tokens.expiresAt - ? new Date(newAuth.tokens.expiresAt) - : null - await connectionStateService.recordAuthSaved({ - connectionId: connection.id, - authExpiresAt, - }) - }, - markOffline: async () => { - const ownerId = await this.resolveOwnerId(connection) - await connectionStateService.markUnhealthy({ - connectionId: connection.id, - ownerId, - }) - }, - } - - // `refreshAuth`/`ensureFreshAuth` only ever read `ctx.auth`/`ctx.authStore` - // (never `ctx.platform`/`ctx.storagePrefix`/`ctx.integrationDetail`) — - // see `Integration.refreshAndPersist` in `@chatbotx.io/sdk`. The - // `platform` stub below is structurally required but never invoked on - // this path. - await adapter.integration.ensureFreshAuth( - { - storagePrefix: "", - auth, - authStore, - platform: { - appUrl: "", - wsUrl: "", - storageUrl: "", - getRealtimeAuthHeaders: async () => ({}), - }, - }, - { force: true }, - ) - - const refreshed = await connectionRepository.findById({ - id: connection.id, - }) - if (!refreshed) { - throw notFoundException("Connection not found") - } - return refreshed - } - - /** Live health check without a refresh cycle — `POST /v1/connections/{id}/verify`. */ - async verify(input: { - connectionId: string - workspaceId: string - }): Promise { - const connection = await this.findOrThrow(input) - if (!isActiveConnectionStatus(connection.status)) { - throw connectionInactiveException() - } - const adapter = this.resolveAdapter(connection.provider) - if (!adapter.store) { - throw connectionNotConfiguredException(connection.provider) - } - const foreignKey = resolveForeignKey(connection) - if (!foreignKey) { - throw connectionNotConfiguredException(connection.provider) - } - - const auth = await adapter.store.loadAuthByForeignKey(foreignKey) - const health = await adapter.provider.verify({ auth }) - const ownerId = await this.resolveOwnerId(connection) - - if (health.ok) { - return await connectionStateService.transition({ - connectionId: connection.id, - event: "verify.ok", - ownerId, - }) - } - if (health.revoked) { - return await connectionStateService.markUnhealthy({ - connectionId: connection.id, - reason: "token_revoked", - ownerId, - }) - } - return await connectionStateService.transition({ - connectionId: connection.id, - event: "verify.failed_non_auth", - reason: "verify_failed", - ownerId, - }) - } - - /** - * `token`/`api_key`/`self_serve` connect: validates `config` against the - * provider's `configFields`, live-validates it via `fromCredentials`, then - * creates (or revives a previously disconnected) `Connection` row plus its - * satellite table row in one transaction. Today every credential-strategy - * provider is `kind: "integration"` (a workspace singleton, no quota - * edge), but the transition still runs through `connectionStateService` - * — not a hardcoded `status: "connected"` insert — so a future `kind: - * "channel"` credential-strategy provider consumes quota correctly too. - */ - async connectFromCredentials(input: { - workspaceId: string - provider: IntegrationType - config: Record - actorUserId?: string | null - /** - * Allows replacing an already-`connected`/`degraded` connection's auth - * and config in place instead of throwing `connectionAlreadyConnected` - * — only for backward-compat upsert aliases (the legacy `PUT - * /v1/integrations/ai/{provider}` route, which has always replaced the - * stored API key/config on repeat calls). The new `POST /v1/connections` - * surface must NOT set this — a fresh connect should reject an existing - * active connection. - */ - allowUpdate?: boolean - }): Promise { - const adapter = this.resolveAdapter(input.provider) - const { provider } = adapter - const isCredentialStrategy = - provider.strategy === "token" || - provider.strategy === "api_key" || - provider.strategy === "self_serve" - if (!(isCredentialStrategy && provider.fromCredentials)) { - throw connectionWrongStrategyException(input.provider) - } - if (!adapter.store) { - throw connectionNotConfiguredException(input.provider) - } - const store = adapter.store - - const parsedConfig = parseConfig(provider.configFields, input.config) - // Fields the caller sent that aren't part of the provider's own - // credential shape (`configFields`) — e.g. an AI provider's `model`/ - // `temperature`/`maxOutputTokens` — flow straight through to the - // satellite row's extra columns via `store.insertRow`'s `config`, - // unvalidated (the satellite table's own NOT NULL/type constraints are - // the validation for those). - const configFieldNames = new Set( - provider.configFields.map((field) => field.name), - ) - const extraConfig = Object.fromEntries( - Object.entries(input.config).filter( - ([key]) => !configFieldNames.has(key), - ), - ) - - let auth: AuthValue - try { - auth = await provider.fromCredentials(parsedConfig) - } catch (err) { - throw connectionCredentialsRejectedException( - toPublicErrorMessage(err, "The provided credentials were rejected."), - ) - } - - const descriptor = provider.describe(auth) - - const existing = await connectionRepository.findByProviderSourceId({ - workspaceId: input.workspaceId, - provider: input.provider, - sourceId: descriptor.sourceId, - }) - if ( - existing && - isActiveConnectionStatus(existing.status) && - !input.allowUpdate - ) { - throw connectionAlreadyConnectedException() - } - - const ownerId = await this.resolveOwnerId({ - kind: provider.kind, - workspaceId: input.workspaceId, - }) - - const connection = await db.transaction(async (tx) => { - // Revive: for a `keep_row` provider (e.g. Telegram, which connects - // via `connectFromCredentials`'s bot-token strategy) the satellite - // row was never deleted on disconnect — calling `store.insertRow` - // again would collide with its own unique constraint and get - // miscategorized as `connectionAlreadyConnectedException` below, - // silently blocking a legitimate revive. Update the kept row's auth - // in place instead, the same primitive `reconnect`/ - // `completeReconnect`/`connectCandidate` already use for this edge. - let integrationId: string | undefined - if (existing && store.onDisconnect === "keep_row") { - const existingForeignKey = resolveForeignKey(existing) - if (existingForeignKey) { - await store.saveAuthByForeignKey(existingForeignKey, auth, tx) - } - integrationId = existing.integrationId ?? undefined - } else { - try { - const inserted = await store.insertRow( - { - workspaceId: input.workspaceId, - auth, - descriptor, - config: extraConfig, - }, - tx, - ) - integrationId = inserted.integrationId - } catch (err) { - if ( - store.duplicateConstraint && - isUniqueViolationError(err, store.duplicateConstraint) - ) { - throw connectionAlreadyConnectedException() - } - throw err - } - } - - if (existing) { - await connectionRepository.update( - { - id: existing.id, - values: { - integrationId: integrationId ?? null, - displayName: descriptor.displayName, - lastError: null, - statusReason: null, - }, - }, - tx, - ) - return await connectionStateService.transition({ - connectionId: existing.id, - event: "connect.completed", - ownerId, - tx, - }) - } - - // Inserted `disconnected` (not `connected`) so the immediately - // following `transition` crosses the inactive->active edge itself — - // that is the only path `ConnectionStateService.transition` consumes - // quota from. Hardcoding `status: "connected"` here would silently - // skip quota consumption for any channel-kind provider connected via - // this method (e.g. a future `token`-strategy channel). - const created = await connectionRepository.insert( - { - workspaceId: input.workspaceId, - provider: input.provider, - kind: provider.kind, - sourceId: descriptor.sourceId, - displayName: descriptor.displayName, - integrationId: integrationId ?? null, - status: "disconnected", - createdBy: input.actorUserId ?? null, - }, - tx, - ) - return await connectionStateService.transition({ - connectionId: created.id, - event: "connect.completed", - ownerId, - tx, - }) - }) - - return await this.subscribeWebhookBestEffort({ - adapter, - auth, - connection, - ownerId, - }) - } - - /** - * `oauth_redirect`/`oauth_popup` connect: creates a `ConnectSession`, then - * builds the provider's `authorizeUrl` with `state = "{sessionId}.{nonce}"` - * — the OAuth callback hub resolves the session from that `state` alone - * (`ConnectSessionService.findByNonce`), before it has any other request - * context. `credential`/`callbackUrl` are resolved by the app-layer caller - * (tenant-aware platform credential + broker/custom-domain callback URL) - * and passed in — this package cannot resolve them itself without - * depending on `apps/builder`. - */ - async startSession(input: { - workspaceId: string - provider: IntegrationType - purpose: ConnectSessionPurpose - credential: ConnectionCredential - callbackUrl: string - targetConnectionId?: string | null - actorUserId?: string | null - actorTokenId?: string | null - platformOwnerId?: string | null - originHost?: string | null - returnUrl?: string | null - }): Promise<{ session: ConnectSessionModel; nextAction: ConnectNextAction }> { - const adapter = this.resolveAdapter(input.provider) - if (!adapter.provider.authorizeUrl) { - throw connectionNotOAuthException(input.provider) - } - - const { session, nonce } = await connectSessionService.create({ - workspaceId: input.workspaceId, - provider: input.provider, - purpose: input.purpose, - targetConnectionId: input.targetConnectionId, - actorUserId: input.actorUserId, - actorTokenId: input.actorTokenId, - platformOwnerId: input.platformOwnerId, - originHost: input.originHost, - returnUrl: input.returnUrl, - }) - - const url = adapter.provider.authorizeUrl({ - credential: input.credential, - callbackUrl: input.callbackUrl, - state: `${session.id}.${nonce}`, - }) - const nextAction: ConnectNextAction = { type: "open_url", url } - const updated = await connectSessionService.submitInput({ - id: session.id, - nextAction, - }) - return { session: updated, nextAction } - } - - /** - * OAuth callback exchange: resolves the session by its `state` nonce, - * exchanges `code` for `auth`, lists connectable candidates, and persists - * them as session targets — `attachAuthorization` always lands on - * `awaiting_selection`; auto-completing a single-target/non-multi-account - * provider is the caller's job (it has the `ConnectionProvider` in scope - * to check `multiAccount` and can immediately follow with - * `connectTargets`). - */ - async completeAuthorization(input: { - sessionId: string - nonce: string - code: string - callbackUrl: string - credential: ConnectionCredential - }): Promise { - const session = await connectSessionService.findByNonce(input.nonce) - if (!session || session.id !== input.sessionId) { - throw connectionStateMismatchException() - } - if (session.status !== "pending") { - throw connectSessionExpiredException( - "This connect session is no longer active.", - ) - } - // No local atomic claim on the session before `exchangeCode` (unlike - // `claimTarget`'s DB-level compare-and-set) — two concurrent calls for - // the same session could both pass this `status === "pending"` check - // and both call `exchangeCode` with the same `code`. Relies on the - // OAuth provider enforcing single-use authorization codes, which every - // provider this connects to does; a genuinely idempotent version would - // need its own compare-and-set (e.g. `pending` -> `authorizing`) here. - - const adapter = this.resolveAdapter(session.provider) - if (!adapter.provider.exchangeCode) { - throw connectionNotOAuthException(session.provider) - } - - let auth: AuthValue - try { - auth = await adapter.provider.exchangeCode({ - code: input.code, - callbackUrl: input.callbackUrl, - credential: input.credential, - }) - } catch (err) { - await connectSessionService.fail({ - id: session.id, - errorCode: "provider_denied", - }) - throw connectionCredentialsRejectedException( - toPublicErrorMessage(err, "The provider rejected the authorization."), - ) - } - - if (session.purpose === "reconnect" && session.targetConnectionId) { - return await this.completeReconnect({ session, auth }) - } - - return await this.listAndAttachCandidates(session, auth) - } - - /** - * Lists connectable candidates for an already-obtained `auth` and - * persists them as the session's `awaiting_selection` targets. Split out - * of `completeAuthorization` so a provider whose credential can be - * satisfied without a fresh OAuth round trip — Messenger's Facebook-SSO - * token reuse (`tryReuseFacebookSsoToken`), which skips the OAuth dialog - * entirely when the user's existing Facebook login already carries every - * required scope — can reach `awaiting_selection` directly from an - * app-layer-constructed `auth`, without a `code`/`nonce` to exchange. - */ - async listAndAttachCandidates( - session: ConnectSessionModel, - auth: AuthValue, - ): Promise { - const adapter = this.resolveAdapter(session.provider) - - let candidates: Awaited< - ReturnType> - > - try { - candidates = adapter.provider.listCandidates - ? await adapter.provider.listCandidates({ auth }) - : [{ ...adapter.provider.describe(auth), auth }] - } catch (err) { - await connectSessionService.fail({ - id: session.id, - errorCode: "provider_error", - }) - throw new Error(toPublicErrorMessage(err, "Failed to list accounts.")) - } - - if (candidates.length === 0) { - await connectSessionService.fail({ - id: session.id, - errorCode: "no_candidates", - }) - throw connectionNoCandidatesException() - } - - const targets = await Promise.all( - candidates.map(async (candidate) => { - if (candidate.alreadyConnected) { - return { - id: candidate.sourceId, - name: candidate.displayName, - avatarUrl: candidate.avatarUrl, - selectable: false, - alreadyConnected: candidate.alreadyConnected, - } - } - const existing = - await connectionRepository.findByProviderAndSourceIdAnyWorkspace({ - provider: session.provider, - sourceId: candidate.sourceId, - }) - if (existing && isActiveConnectionStatus(existing.status)) { - const scope: "this_workspace" | "other_workspace" = - existing.workspaceId === session.workspaceId - ? "this_workspace" - : "other_workspace" - return { - id: candidate.sourceId, - name: candidate.displayName, - avatarUrl: candidate.avatarUrl, - selectable: false, - alreadyConnected: scope, - } - } - return { - id: candidate.sourceId, - name: candidate.displayName, - avatarUrl: candidate.avatarUrl, - selectable: true, - } - }), - ) - - // Encrypts the full candidate list — not just the exchanged `auth` — so - // each candidate's own distinct `auth` (a multi-account provider's - // per-page token, e.g. Messenger) survives to `connectTargets`. For a - // single-target/`describe()`-fallback provider this is a one-element - // array holding the same `auth` `exchangeCode` returned. AAD binds the - // ciphertext to this exact session so it cannot be replayed against - // another session's row. - const encryptedAuth = await encryptUtils.encryptObject( - candidates, - `connect-session:${session.id}`, - ) - return await connectSessionService.attachAuthorization({ - id: session.id, - encryptedAuth, - targets, - }) - } - - /** - * Re-authorizes an existing (typically `needs_reauth`) `Connection` — a - * `startSession` with `purpose: "reconnect"` and `targetConnectionId` set, - * so `completeAuthorization` skips candidate selection entirely and - * verifies the re-granted account's identity matches this exact - * connection instead. - */ - async reconnect(input: { - connectionId: string - workspaceId: string - credential: ConnectionCredential - callbackUrl: string - actorUserId?: string | null - actorTokenId?: string | null - platformOwnerId?: string | null - originHost?: string | null - returnUrl?: string | null - }): Promise<{ session: ConnectSessionModel; nextAction: ConnectNextAction }> { - const connection = await this.findOrThrow({ - connectionId: input.connectionId, - workspaceId: input.workspaceId, - }) - return await this.startSession({ - workspaceId: input.workspaceId, - provider: connection.provider, - purpose: "reconnect", - credential: input.credential, - callbackUrl: input.callbackUrl, - targetConnectionId: connection.id, - actorUserId: input.actorUserId, - actorTokenId: input.actorTokenId, - platformOwnerId: input.platformOwnerId, - originHost: input.originHost, - returnUrl: input.returnUrl, - }) - } - - /** - * `completeAuthorization`'s reconnect path: verifies the freshly - * re-authorized identity (`provider.describe(auth).sourceId`) matches the - * target `Connection`'s own `sourceId` — a user can grant access to a - * DIFFERENT account than the one being reconnected, which must not - * silently overwrite the wrong connection's auth — then saves the new - * auth and transitions the connection back to healthy. - */ - private async completeReconnect(input: { - session: ConnectSessionModel - auth: AuthValue - }): Promise { - const { session, auth } = input - const targetConnectionId = session.targetConnectionId - if (!targetConnectionId) { - throw notFoundException("Connection not found") - } - const connection = await connectionRepository.findById({ - id: targetConnectionId, - }) - if (!connection) { - await connectSessionService.fail({ - id: session.id, - errorCode: "internal_error", - }) - throw notFoundException("Connection not found") - } - - const adapter = this.resolveAdapter(connection.provider) - const descriptor = adapter.provider.describe(auth) - if (descriptor.sourceId !== connection.sourceId) { - await connectSessionService.fail({ - id: session.id, - errorCode: "provider_denied", - }) - throw connectionIdentityMismatchException() - } - - const foreignKey = resolveForeignKey(connection) - if (!(adapter.store && foreignKey)) { - await connectSessionService.fail({ - id: session.id, - errorCode: "internal_error", - }) - throw connectionNotConfiguredException(connection.provider) - } - const store = adapter.store - - const authExpiresAt = - auth.authType === "oauth2" && auth.tokens.expiresAt - ? new Date(auth.tokens.expiresAt) - : null - // `connect.completed`, not `auth.saved`/`recordAuthSaved` — `auth.saved` - // requires the connection to already be ACTIVE (`connected`/`degraded`) - // and throws otherwise (`state.ts`), but reconnect's whole purpose is - // reviving an INACTIVE (`needs_reauth`/`disconnected`) connection. - // `connect.completed` is the FSM event that actually allows that edge - // (and consumes quota on it for a channel-kind connection) — the same - // event `connectFromCredentials`'s revive path uses. - const ownerId = await this.resolveOwnerId(connection) - await db.transaction(async (tx) => { - await store.saveAuthByForeignKey(foreignKey, auth, tx) - await connectionRepository.update( - { id: connection.id, values: { authExpiresAt, lastError: null } }, - tx, - ) - await connectionStateService.transition({ - connectionId: connection.id, - event: "connect.completed", - ownerId, - tx, - }) - }) - - return await connectSessionService.recordResults({ - id: session.id, - results: [ - { - targetId: connection.sourceId, - status: "connected", - connectionId: connection.id, - }, - ], - resultConnectionIds: [connection.id], - }) - } - - /** - * `instagramFacebook`'s satellite table is shared with `instagram` - * (`IntegrationInstagram`, disambiguated by its `type` column — see - * `CONNECTION_STORE_BINDINGS`), but `Inbox.channel` has no matching - * `instagramFacebook` value (`ChannelType` only has `instagram`) — every - * other channel-kind `IntegrationType` literal is already a valid - * `ChannelType`. Only called for `kind === "channel"` providers. - */ - private toChannelType(provider: IntegrationType): ChannelType { - return provider === "instagramFacebook" - ? "instagram" - : (provider as ChannelType) - } - - /** - * Connects one already-authorized `ConnectionCandidate` — the per-target - * unit of work behind `connectTargets`. Channel-kind candidates mint (or - * revive) their `Inbox` row first via `inboxService.create`, since - * `Connection.inboxId` must exist before `store.insertRow` and before - * `connectionStateService.transition`'s own Inbox mirror can run; - * `skipQuota: true` there is required — `transition`'s `applyQuotaEdge` - * is the sole quota consumption point on this path (see `ConnectionService.connectFromCredentials`'s - * revive-or-insert comment above), so also consuming inside - * `inboxService.create` would charge a brand-new channel twice. Otherwise - * mirrors `connectFromCredentials`'s revive-or-insert transaction body. - * - * Scope: a *bare* connect. The per-provider UI conveniences a - * picker-driven connect layers on top (Messenger's persistent-menu - * branding, workspace-logo push, tag-sync enqueue — see - * `apps/builder/src/features/integration-messenger/actions/connect-page.ts`) - * are NOT replicated here; `ConnectionProvider.actions` (reserved) is the - * intended future home for a client to opt into any of those separately. - */ - private async connectCandidate(input: { - workspaceId: string - provider: IntegrationType - candidate: ConnectionCandidate - actorUserId?: string | null - }): Promise { - const adapter = this.resolveAdapter(input.provider) - const { provider } = adapter - if (!adapter.store) { - throw connectionNotConfiguredException(input.provider) - } - const store = adapter.store - const auth = input.candidate.auth as AuthValue - const descriptor = provider.describe(auth) - const extraConfig = provider.candidateToConfig?.(auth) ?? {} - - const existing = await connectionRepository.findByProviderSourceId({ - workspaceId: input.workspaceId, - provider: input.provider, - sourceId: descriptor.sourceId, - }) - if (existing && isActiveConnectionStatus(existing.status)) { - throw connectionAlreadyConnectedException() - } - - const ownerId = await this.resolveOwnerId({ - kind: provider.kind, - workspaceId: input.workspaceId, - }) - - const connection = await db.transaction(async (tx) => { - let inboxId: string | undefined - if (provider.kind === "channel") { - if (!ownerId) { - throw notFoundException("Workspace owner not found") - } - const { inbox } = await inboxService.create({ - data: { - workspaceId: input.workspaceId, - channel: this.toChannelType(input.provider), - sourceId: descriptor.sourceId, - name: descriptor.displayName, - }, - ownerId, - tx, - skipQuota: true, - }) - inboxId = inbox.id - } - - // Revive: for a `keep_row` provider (tiktok/whatsapp/zalo/telegram/ - // webchat) the satellite row was never deleted on disconnect — - // calling `store.insertRow` again would collide with its own unique - // constraint and get miscategorized as `connectionAlreadyConnectedException` - // below, silently blocking a legitimate revive. Update the kept row's - // auth in place instead, the same primitive `reconnect`/ - // `completeReconnect` already use for this edge. A `delete_row` - // provider's satellite row IS gone even when `existing` is set (the - // Connection row outlives its satellite row across a disconnect), so - // it still needs a fresh `insertRow`. - let integrationId: string | undefined - if (existing && store.onDisconnect === "keep_row") { - const existingForeignKey = resolveForeignKey(existing) - if (existingForeignKey) { - await store.saveAuthByForeignKey(existingForeignKey, auth, tx) - } - integrationId = existing.integrationId ?? undefined - } else { - try { - const inserted = await store.insertRow( - { - workspaceId: input.workspaceId, - inboxId, - auth, - descriptor, - config: extraConfig, - }, - tx, - ) - integrationId = inserted.integrationId - } catch (err) { - if ( - store.duplicateConstraint && - isUniqueViolationError(err, store.duplicateConstraint) - ) { - throw connectionAlreadyConnectedException() - } - throw err - } - } - - if (existing) { - await connectionRepository.update( - { - id: existing.id, - values: { - inboxId: inboxId ?? existing.inboxId, - integrationId: integrationId ?? null, - displayName: descriptor.displayName, - lastError: null, - statusReason: null, - }, - }, - tx, - ) - return await connectionStateService.transition({ - connectionId: existing.id, - event: "connect.completed", - ownerId, - tx, - }) - } - - const created = await connectionRepository.insert( - { - workspaceId: input.workspaceId, - provider: input.provider, - kind: provider.kind, - sourceId: descriptor.sourceId, - displayName: descriptor.displayName, - inboxId: inboxId ?? null, - integrationId: integrationId ?? null, - status: "disconnected", - createdBy: input.actorUserId ?? null, - }, - tx, - ) - return await connectionStateService.transition({ - connectionId: created.id, - event: "connect.completed", - ownerId, - tx, - }) - }) - - return await this.subscribeWebhookBestEffort({ - adapter, - auth, - connection, - ownerId, - }) - } - - /** - * Finishes an `awaiting_selection` connect session: atomically claims - * each requested target (safe against a double-submit or two tabs — a - * target claimed by a prior call maps to a `duplicated` outcome, never a - * second connect), then connects it via `connectCandidate`. Never throws - * for a single target's failure — every outcome (`connected`/ - * `duplicated`/`limitReached`/`failed`) is reported back per-target, the - * same vocabulary `CONNECT_ITEM_STATUSES` uses for the picker flows this - * replaces. - */ - async connectTargets(input: { - sessionId: string - workspaceId: string - targetIds: string[] - actorUserId?: string | null - }): Promise<{ - session: ConnectSessionModel - connections: ConnectionModel[] - outcomes: ConnectSessionOutcome[] - }> { - const session = await connectSessionService.findByIdForWorkspace({ - id: input.sessionId, - workspaceId: input.workspaceId, - }) - if (!session) { - throw notFoundException("Connect session not found") - } - if (session.status !== "awaiting_selection" || !session.encryptedAuth) { - throw connectSessionExpiredException( - "This connect session is not awaiting target selection.", - ) - } - - const candidates = await encryptUtils.decryptObject( - session.encryptedAuth, - encryptedCandidatesSchema, - `connect-session:${session.id}`, - ) - const candidateBySourceId = new Map( - candidates.map((candidate) => [candidate.sourceId, candidate]), - ) - - const outcomes: ConnectSessionOutcome[] = [] - const connections: ConnectionModel[] = [] - - for (const targetId of input.targetIds) { - const target = session.targets.find((t) => t.id === targetId) - const candidate = candidateBySourceId.get(targetId) - if (!(target && candidate)) { - outcomes.push({ targetId, status: "failed", reason: "unknown" }) - continue - } - if (!target.selectable) { - outcomes.push( - target.alreadyConnected - ? { targetId, status: "duplicated", reason: "alreadyConnected" } - : { targetId, status: "failed", reason: "notSelectable" }, - ) - continue - } - - const claimed = await connectSessionService.claimTarget({ - id: session.id, - targetId, - }) - if (!claimed) { - outcomes.push({ - targetId, - status: "duplicated", - reason: "alreadyConnected", - }) - continue - } - - try { - const connection = await this.connectCandidate({ - workspaceId: input.workspaceId, - provider: session.provider, - candidate: candidate as ConnectionCandidate, - actorUserId: input.actorUserId, - }) - connections.push(connection) - outcomes.push({ - targetId, - status: "connected", - connectionId: connection.id, - }) - } catch (err) { - if ( - err instanceof ChatbotXException && - err.code === "channelLimitReached" - ) { - outcomes.push({ - targetId, - status: "limitReached", - reason: "workspaceLimit", - }) - } else if ( - err instanceof ChatbotXException && - err.code === "connectionAlreadyConnected" - ) { - outcomes.push({ - targetId, - status: "duplicated", - reason: "alreadyConnected", - }) - } else { - logger.warn( - { err, targetId, provider: session.provider }, - "connectTargets: candidate connect failed", - ) - outcomes.push({ - targetId, - status: "failed", - reason: "providerRejected", - detail: toPublicErrorMessage(err, "Connect failed"), - }) - } - } - } - - const updatedSession = await connectSessionService.recordResults({ - id: session.id, - results: outcomes, - resultConnectionIds: connections.map((connection) => connection.id), - }) - - return { session: updatedSession, connections, outcomes } - } + disconnect = disconnect + refresh = refresh + verify = verify + connectFromCredentials = connectFromCredentials + reconnect = reconnect + startSession = startSession + completeAuthorization = completeAuthorization + listAndAttachCandidates = listAndAttachCandidates + connectTargets = connectTargets } export const connectionService = new ConnectionService() diff --git a/packages/database/__tests__/connection-repository.test.ts b/packages/database/__tests__/connection-repository.test.ts new file mode 100644 index 0000000000..a50720b234 --- /dev/null +++ b/packages/database/__tests__/connection-repository.test.ts @@ -0,0 +1,51 @@ +import { beforeEach, describe, expect, test, vi } from "vitest" + +const mocks = vi.hoisted(() => ({ + findMany: vi.fn(async () => []), + eq: vi.fn((column: unknown, value: unknown) => ({ eq: [column, value] })), + relationsFilterToSQL: vi.fn(() => ({})), + $count: vi.fn(async () => 0), +})) + +vi.mock("../src/client", () => ({ + db: { + query: { connectionModel: { findMany: mocks.findMany } }, + $count: mocks.$count, + }, + eq: mocks.eq, + relationsFilterToSQL: mocks.relationsFilterToSQL, +})) + +vi.mock("../src/schema", () => ({ + connectionModel: { id: "id" }, +})) + +vi.mock("../src/utils", () => ({ + getPaginationWithDefaults: vi.fn(() => ({ limit: 20, offset: 0 })), +})) + +const { connectionRepository } = await import( + "../src/repositories/connection/repository" +) + +beforeEach(() => { + vi.clearAllMocks() + mocks.findMany.mockResolvedValue([]) +}) + +describe("connectionRepository.list", () => { + test("orders deterministically by kind, provider, displayName, id (T5)", async () => { + await connectionRepository.list({ workspaceId: "workspace-1" }) + + expect(mocks.findMany).toHaveBeenCalledWith( + expect.objectContaining({ + orderBy: { + kind: "asc", + provider: "asc", + displayName: "asc", + id: "asc", + }, + }), + ) + }) +}) diff --git a/packages/database/__tests__/integration/insert-required-columns.test.ts b/packages/database/__tests__/integration/insert-required-columns.test.ts index 61bdacb0c1..edc1960fe3 100644 --- a/packages/database/__tests__/integration/insert-required-columns.test.ts +++ b/packages/database/__tests__/integration/insert-required-columns.test.ts @@ -35,11 +35,13 @@ import { getTableColumns, getTableName } from "drizzle-orm" import type { PgTable } from "drizzle-orm/pg-core" import { Client } from "pg" import { afterAll, beforeAll, describe, expect, test, vi } from "vitest" +import { connectSessionRepository } from "../../src/repositories/connect-session/repository" import { integrationInstagramRepository } from "../../src/repositories/integration-instagram/repository" import { integrationMessengerRepository } from "../../src/repositories/integration-messenger/repository" import { integrationWhatsappRepository } from "../../src/repositories/integration-whatsapp/repository" import { whatsappSignupSessionRepository } from "../../src/repositories/integration-whatsapp/signup-session" import { + connectSessionModel, inboxModel, integrationInstagramModel, integrationMessengerModel, @@ -72,6 +74,7 @@ type ColumnFact = { type TableFacts = Map const AUDITED_TABLES = [ + connectSessionModel, inboxModel, integrationInstagramModel, integrationMessengerModel, @@ -253,6 +256,25 @@ describe.skipIf(!databaseUrl)( tx, ), }, + { + name: "connectSessionRepository.insert", + table: connectSessionModel as unknown as PgTable, + run: (tx) => + connectSessionRepository.insert( + { + workspaceId: "workspace-1", + provider: "telegram", + purpose: "connect", + stateNonceHash: "nonce-hash", + targets: [], + claimedTargetIds: [], + resultConnectionIds: [], + results: [], + expiresAt: new Date(), + }, + tx, + ), + }, ] test.each(cases)("$name writes every column the database requires", async ({ diff --git a/packages/database/__tests__/integration/schema-default-parity.test.ts b/packages/database/__tests__/integration/schema-default-parity.test.ts index 403f7e9fcd..5d27f14ccd 100644 --- a/packages/database/__tests__/integration/schema-default-parity.test.ts +++ b/packages/database/__tests__/integration/schema-default-parity.test.ts @@ -61,6 +61,10 @@ const DEFAULTS_MISSING_FROM_DATABASE = [ "AIAgent.tools", "AIMCPServer.selectedTools", "AutomatedResponse.keywords", + "ConnectSession.claimedTargetIds", + "ConnectSession.resultConnectionIds", + "ConnectSession.results", + "ConnectSession.targets", "Folder.paths", "IntegrationInstagram.conversationStarters", "IntegrationInstagram.persistentMenus", diff --git a/packages/database/src/schema/connect-session.ts b/packages/database/src/schema/connect-session.ts index 9a335d47ac..b30ab91de8 100644 --- a/packages/database/src/schema/connect-session.ts +++ b/packages/database/src/schema/connect-session.ts @@ -36,7 +36,8 @@ import { workspaceApiTokenModel } from "./workspace-api-token" * device-code, OAuth1, or "enter verification PIN" strategy is a new * `nextAction.type`, not a new table. Generalises `WhatsappSignupSession` * (kept, wrapped — its id goes in `encryptedAuth`) and the three - * `fb_*_pending_auth` cookies (`apps/builder/src/lib/facebook-pending-auth.ts`). + * `fb_*_pending_auth` cookies this PR deletes in favor of this table + * (formerly `apps/builder/src/lib/facebook-pending-auth.ts`). */ export const connectSessionModel = pgTable( "ConnectSession", diff --git a/packages/sdk/src/lib/exception.ts b/packages/sdk/src/lib/exception.ts index b11769c3e8..17e2663273 100644 --- a/packages/sdk/src/lib/exception.ts +++ b/packages/sdk/src/lib/exception.ts @@ -85,3 +85,18 @@ export class AuthRefreshException extends SdkException { } } } + +/** + * Duck-typed 401/403 check shared by every REST-based marketing-integration + * provider's `isRevokedTokenError`/`verify` catch (Mailchimp, Klaviyo, + * MailerLite, SendGrid, Drip, ActiveCampaign, GetResponse). Each provider's + * own API error class (`MailchimpApiError`, `DripApiError`, …) carries its + * own `statusCode` field rather than sharing one common base, so this checks + * the shape, not a specific class — `error instanceof X` would need one + * import per provider for the exact same two-line check. + */ +export const isUnauthorizedStatusError = (error: unknown): boolean => + typeof error === "object" && + error !== null && + "statusCode" in error && + (error.statusCode === 401 || error.statusCode === 403) diff --git a/packages/sdk/src/lib/integration.ts b/packages/sdk/src/lib/integration.ts index b39bd9223c..77cba31b28 100644 --- a/packages/sdk/src/lib/integration.ts +++ b/packages/sdk/src/lib/integration.ts @@ -641,11 +641,15 @@ export class Integration< /** * Public entry point for the Connection domain to force or proactively - * refresh auth outside the request-triggered `invokeWithRefresh` flow - * (the `refresh-connections` cron, `POST /v1/connections/{id}/refresh`). - * With no `opts`, behaves like the proactive check used before every - * handler call (refresh only when within {@link AUTH_REFRESH_BUFFER_MS} of - * expiry). `force: true` always calls `refreshAuth` regardless of expiry. + * refresh auth outside the request-triggered `invokeWithRefresh` flow — + * today called only from `POST /v1/connections/{id}/refresh` + * (`ConnectionService.refresh`). `ConnectionStateService.listDueForRefresh` + * exists but has no scheduled caller yet; a `refresh-connections` cron + * that proactively calls this method for rows it returns is still + * unbuilt (Phase 4). With no `opts`, behaves like the proactive check + * used before every handler call (refresh only when within + * {@link AUTH_REFRESH_BUFFER_MS} of expiry). `force: true` always calls + * `refreshAuth` regardless of expiry. */ async ensureFreshAuth( ctx: Context, diff --git a/packages/utils/src/connection.ts b/packages/utils/src/connection.ts index 1d24dda74b..d60cf62516 100644 --- a/packages/utils/src/connection.ts +++ b/packages/utils/src/connection.ts @@ -107,6 +107,7 @@ export const connectSessionErrorCodes = z.enum([ "state_mismatch", "expired", "provider_denied", + "exchange_failed", "provider_error", "no_candidates", "already_connected", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4c34583fc5..428700e03a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1361,12 +1361,18 @@ importers: '@chatbotx.io/typescript-config': specifier: workspace:* version: link:../../packages/typescript-config + '@chatbotx.io/vitest-config': + specifier: workspace:* + version: link:../../packages/vitest-config '@types/node': specifier: 24.x version: 24.13.1 typescript: specifier: ^5 version: 5.9.3 + vitest: + specifier: ^4.1.0 + version: 4.1.8(@opentelemetry/api@1.9.1)(@types/node@24.13.1)(@vitest/coverage-v8@4.1.8)(jsdom@25.0.1)(msw@2.14.6(@types/node@24.13.1)(typescript@5.9.3))(vite@7.3.5(@types/node@24.13.1)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.23.0)(yaml@2.9.0)) integrations/mailer-lite: dependencies: