diff --git a/apps/docs/content/docs/search/confluence.mdx b/apps/docs/content/docs/search/confluence.mdx
index f818dcba9de..8943a97d197 100644
--- a/apps/docs/content/docs/search/confluence.mdx
+++ b/apps/docs/content/docs/search/confluence.mdx
@@ -31,21 +31,16 @@ Teammates authorize Sim's shared Confluence app, which also requests workflow pe
### Choose Confluence
-Open your organization's **Settings → Sources**, turn on **Confluence**, then select **Set up** (or **Manage**) → **Add source**. This opens service-account setup.
+Open **Settings → Sources → Add source** and select **Confluence**. This opens **Connect Confluence site** with service-account authentication. To connect another site later, open **Confluence** from the Sources list and select **Add Confluence site**.
### Choose the account and spaces
-Under **Indexing account**, select a service account or [add one](#using-a-service-account). Enter the same **Confluence Domain** as the credential, then choose **Spaces**. To enter comma-separated keys such as `ENG, PRODUCT`, use the switch beside the Spaces field.
+Under **Service account**, select a service account or [add one](#using-a-service-account). Enter the same **Confluence site** as the credential, then choose **Spaces**. **All** in the dropdown selects every space the account can currently browse; newly created spaces are not added automatically. Clear the picker search before selecting all.
-
+To enter comma-separated keys such as `ENG, PRODUCT`, use the switch beside **Spaces**. Switching between the picker and manual entry keeps your selection.
Open **More options** for content type, labels, and metadata tags. The default is **Pages only**; choose **All content** to include blog posts.
@@ -65,9 +60,22 @@ For workspace Search, start from **Search → Add source**. Available methods de
## Connect member accounts
-After an admin allows Confluence, open **Integrations → Connect**. If no source exists, enter **Confluence Domain** and **Space Keys**, then connect your account. To use another site or space scope, choose the row labeled **Connect a different site or content scope**.
+After an admin allows Confluence, open **Integrations → Connect**. For an existing source, authorize your account; you do not choose its spaces again.
+
+If no source exists, or you choose **Connect a different site or content scope**:
+
+1. Under **Your account**, select a saved account or choose **Connect account**. Authorize using the Atlassian email matching your verified Sim email.
+2. Enter the hostname under **Atlassian site**, then choose **Spaces**. Use **All** in the dropdown for the complete current list, or **Enter keys manually** for comma-separated keys. You can select up to 1,000 spaces in this form.
+3. Select **Connect & Sync**. Sim saves the selected scope and starts indexing with your account.
+
+
-Admins manage these sources under **Settings → Sources → Confluence → Manage**. An **Account for browsing** populates the space picker; it does not enroll that account for Search. Manual space keys work without a browsing account.
+Admins manage these sources under **Settings → Sources → Confluence**. When configuring a member-account connection, an **Account for browsing** populates the space picker; a saved personal Search account can be reused here. Choosing a browsing account does not enroll it for Search. Manual space keys work without a browsing account.
## Using a service account
@@ -95,7 +103,7 @@ read:group:confluence
Use all 12 scopes for account validation, pickers, content, permissions, and directory reads. Central indexing does not need write scopes.
4. Review and create the token, then copy it. Atlassian shows it only once.
-5. In Sim's source form, use **Indexing account** to add a service account. Paste the **API token**, enter **Site domain** (hostname only), and select **Add service account**. Continue in the source form with the same domain.
+5. In Sim's source form, use **Service account** to add a service account. Paste the **API token**, enter **Site domain** (hostname only), and select **Add service account**. Continue in the source form with the same domain.
-See Atlassian's [account setup](https://support.atlassian.com/user-management/docs/manage-your-service-accounts/) and [token instructions](https://support.atlassian.com/user-management/docs/manage-api-tokens-for-service-accounts/). Scopes do not grant space or page access. To replace an expiring token or change scopes, add a new credential in the source's **Settings**, select **Change indexing account**, and verify a sync before revoking the old token.
+See Atlassian's [account setup](https://support.atlassian.com/user-management/docs/manage-your-service-accounts/) and [token instructions](https://support.atlassian.com/user-management/docs/manage-api-tokens-for-service-accounts/). Scopes do not grant space or page access. To replace an expiring token or change scopes, add a new credential in the source's **Settings**, select **Change service account**, and verify a sync before revoking the old token.
## Configuration and indexed content
| Setting | What it controls |
| --- | --- |
-| **Confluence Domain** | Cloud hostname only, such as `your-team.atlassian.net`; omit page URLs and `/wiki`. |
+| **Confluence site** | Cloud hostname only, such as `your-team.atlassian.net`; omit page URLs and `/wiki`. |
| **Spaces / Space Keys** | Required spaces. The picker and manual input set the same scope. |
| **Content Type** | **Pages only** (default), **Blog posts only**, or **All content** for both. |
| **Filter by Label** | Optional comma-separated labels; content can match any listed label. |
@@ -122,7 +130,7 @@ Search manages the schedule and hides item limits. It indexes published/current
Central sources combine space permissions, page and ancestor restrictions, and group membership. Before returning central content, Sim uses your personal connection to check that you still have access to the configured Confluence site. If Atlassian cannot confirm that access, the content is hidden. Member sources use each person's provider listing. Sim admin status does not grant access to all pages, and permission changes take effect after syncing and processing.
-Open **Settings → Sources → Confluence → Manage**, then a source's **Documents**, **Settings**, or **Sync history**. Invite teammates through **Settings → Members → Invite** or SSO, then have them connect through **Integrations**. **Accounts → Request connections** only requests a provider connection; it does not invite people to the organization.
+Open **Settings → Sources → Confluence**, then a source's **Documents**, **Settings**, or **Sync history**. Invite teammates through **Settings → Members → Invite** or SSO, then have them connect through **Integrations**. **People → Request connections** only requests a provider connection; it does not invite people to the organization.
## Troubleshooting
@@ -147,6 +155,6 @@ Configure one shared Confluence OAuth app for teammates' connections:
2. Under **Authorization → OAuth 2.0 (3LO)**, add `https:///api/auth/oauth2/callback/confluence` as a callback.
3. Under **Permissions**, add the Confluence API and its full `confluence` scope list from [Sim's OAuth configuration](https://github.com/simstudioai/sim/blob/staging/apps/sim/lib/oauth/oauth.ts), including `read:group:confluence`. Add **User Identity API → read:me**. Sim requests `offline_access` for refresh tokens; the service-account list above does not replace this shared OAuth scope set.
4. Enable sharing under **Distribution**. Set `CONFLUENCE_CLIENT_ID` and `CONFLUENCE_CLIENT_SECRET` from the app's **Settings**, verify `NEXT_PUBLIC_APP_URL`, and restart Sim.
-5. Connect from **Integrations** and select the configured site. After changing the OAuth client or requested scopes, use **Settings → Sources → More → Refresh connection settings**, then have affected teammates reconnect.
+5. Connect from **Integrations** and select the configured site. After changing the OAuth client or requested scopes, use **Settings → Sources → More → Update sign-in settings**, then have affected teammates reconnect.
The callback must match Sim's scheme, hostname, port, and path exactly. If only the app owner can connect, check **Distribution**. See the [Atlassian OAuth guide](https://developer.atlassian.com/cloud/confluence/oauth-2-3lo-apps/) and [Sim deployment reference](/platform/self-hosting/integrations-oauth).
diff --git a/apps/docs/content/docs/search/connect-your-account.mdx b/apps/docs/content/docs/search/connect-your-account.mdx
index 7b031e1cf82..bfe41deae8f 100644
--- a/apps/docs/content/docs/search/connect-your-account.mdx
+++ b/apps/docs/content/docs/search/connect-your-account.mdx
@@ -23,7 +23,7 @@ For a central Google source, use your primary Workspace email and open **Search*
## Open Integrations
-Open **Integrations** in the main sidebar, find the provider or source, and select **Connect**. Your first connection may ask for a GitHub repository, Confluence domain and space keys, or Jira domain and project keys. Enter the required fields and select **Connect**. If the provider is missing, ask an organization admin to turn it on under **Settings → Sources**.
+Open **Integrations** in the main sidebar, find the provider or source, and select **Connect**. First-time setup can also ask which content to index. For [Confluence](/search/confluence) and [Jira](/search/jira), connect your Atlassian account first, then choose the site and spaces or projects and select **Connect & Sync**. [GitHub](/search/github) asks for a repository. If the provider is missing, ask an organization admin to add it under **Settings → Sources → Add source**.
To connect another supported repository, site, or project scope, find the provider row labeled **Connect a different site or content scope** and select **Connect**. Connecting an existing source does not ask you to configure it again.
@@ -36,7 +36,7 @@ To connect another supported repository, site, or project scope, find the provid
Complete the provider's authorization in the new tab. Choose the account associated with your verified Sim email. The provider may require your organization's SSO or app approval.
-The authorization tab closes when the connection completes and Integrations updates. If the tab stays open, return to Integrations. Your account is saved when authorization completes; there is no separate submit step. If the popup was blocked or closed, allow popups and select **Connect** again. While authorization is pending, use **Open again**.
+The authorization tab closes when the connection completes and Integrations updates. If the tab stays open, return to Integrations. For an existing source, your account is saved when authorization completes. For first-time Atlassian setup, return to the form, choose your content, and select **Connect & Sync**. If the popup was blocked or closed, allow popups and select **Connect** again. While authorization is pending, use **Open again**.
@@ -74,7 +74,7 @@ An account connection request does not invite you into the Sim organization. You
On the main **Integrations** page, use **Reconnect** beside an expired connection to renew it. To withdraw an account, open its row's actions menu and select **Disconnect**, then confirm. If several accounts are connected, choose the account to disconnect. Disconnecting stops that account from being used for organization indexing and workflows, and removes Search access that depends on it.
-Admins manage setup from **Settings → Sources**. Select **Manage** beside the integration. Providers with personal connections have **Accounts** and a source list under **Advanced** (Google) or **Sources**. Without personal connections, the source list opens directly. This does not grant the admin access to every document.
+Admins manage setup from **Settings → Sources**. Open an integration, then its connection to see **Documents**, **Settings**, and **Sync history**. **People** shows account contributors across integrations and supports filtering by integration. This does not grant the admin access to every document.
## If you get stuck
@@ -87,7 +87,7 @@ Admins manage setup from **Settings → Sources**. Select **Manage** beside the
| **Verify email** | Verify your Sim email to return to the connection page. Reopen the original link if you are not redirected. |
| Expired or cancelled authorization | Return to the original connection page and start again. If the invitation itself expired, ask the admin for a new request. |
| Access revoked | Ask the organization admin to restore your account contribution access before reconnecting. |
-| Needs admin attention | Ask your admin to open **Settings → Sources**, select **Manage** beside the integration, and open its source or sync configuration to inspect the error. |
+| Needs admin attention | Ask your admin to open **Settings → Sources**, select the integration, and open its connection to inspect the error. |
Your Sim role does not override document access at the source. Connecting a different account or receiving a Search link does not share someone else's mailbox, private calendar, or restricted documents with you.
diff --git a/apps/docs/content/docs/search/github.mdx b/apps/docs/content/docs/search/github.mdx
index 9a69063e165..8c3da90651a 100644
--- a/apps/docs/content/docs/search/github.mdx
+++ b/apps/docs/content/docs/search/github.mdx
@@ -9,7 +9,7 @@ import { Image } from '@/components/ui/image'
GitHub Search indexes text files from repositories on `github.com`. An organization admin can install the GitHub App once and use it to index selected repositories. Each person connects their own GitHub account once to search the repositories they can access. Installing the App does not connect teammates or give them the installer's permissions.
-Admin setup uses your organization's **Settings → Sources** page. Teammates connect from **Integrations** in the main sidebar. Installation indexing is available for organization Search. For workspace Search, use **Search → Add source** with member accounts or a dedicated user account; **Create & Invite** is the workspace equivalent of **Add source**.
+Admin setup uses your organization's **Settings → Sources** page. Teammates connect from **Integrations** in the main sidebar. Installation indexing is available for organization Search. For workspace Search, use **Search → Add source** with member accounts or a dedicated user account.
## Before you start
@@ -26,7 +26,7 @@ To connect an installation for central indexing, you must be a Sim organization
### Open GitHub setup
-Open **Settings → Sources** and turn on **GitHub**. Select **Set up** (or **Manage** if sources already exist), then **Add source**.
+Open **Settings → Sources → Add source** and select **GitHub**. This opens **Add GitHub repository**. Add one repository per connection; to add another, open **GitHub** from the Sources list and select **Add repository**.
@@ -35,8 +35,6 @@ Open **Settings → Sources** and turn on **GitHub**. Select **Set up** (or **Ma
In **Sync documents with**, choose **Connect GitHub App** to index through an installation:
-
-
1. Select **Connect your GitHub account** if prompted. Finish authorization in the new tab, then return and select **Refresh**.
@@ -55,20 +53,18 @@ Alternatively, leave **Connected members** selected to use members' accounts for
### Choose what to index
-Enter **Repository** as `owner/repo`. For installation indexing, it must belong to the installation's account and be included in the repositories granted to the App. Add one source per repository; installing on all repositories does not automatically create sources for them.
+Enter **Repository** as `owner/repo`. For installation indexing, it must belong to the installation's account and be included in the repositories granted to the App. Add one connection per repository; installing on all repositories does not automatically add them to Search.
Open **More options** if you need a different branch, path or extension filters, or metadata tags.
-
-
| Field | What to enter |
|---|---|
-| Repository | `owner/repo`. Add another source for another repository. |
+| Repository | `owner/repo`. Use **Add repository** for another repository. |
| Branch | Optional. Leave blank to follow the repository's default branch. |
| Path Filter | Optional prefix such as `docs/`. |
| File Extensions | Optional comma-separated list, such as `.md, .txt, .mdx`. |
-**Metadata tags** controls the metadata stored with results. Its defaults are suitable for most sources. Select **Add source** to save the source.
+**Metadata tags** controls the metadata stored with results. Its defaults are suitable for most sources. Select **Add repository** to save the connection.
@@ -79,9 +75,9 @@ Open **Integrations** in the main sidebar, select **Connect** on the GitHub sour
An App installation or dedicated indexing account can start syncing after the source is saved. With **Connected members**, indexing begins after someone connects. Each teammate still connects before searching. An existing GitHub connection in the same Sim organization is reused across its GitHub sources.
-Open **Settings → Sources**, select **Manage** beside **GitHub**, then open the source to inspect **Documents**, edit **Settings**, or review **Sync history**.
+Open **Settings → Sources**, open **GitHub**, then open the source to inspect **Documents**, edit **Settings**, or review **Sync history**.
-Use **GitHub → Accounts → Request connections** to send provider-specific connection requests. These requests do not grant organization membership. For another repository, add another source; members can also select **Connect** beside the GitHub row labeled **Connect a different site or content scope** in Integrations.
+Use **Settings → Sources → People**, filter by **GitHub**, then select **Request connections** to send provider-specific connection requests. These requests do not grant organization membership. For another repository, add another source; members can also select **Connect** beside the GitHub row labeled **Connect a different site or content scope** in Integrations.
@@ -106,7 +102,7 @@ This is an installation plus personal authorization flow. GitHub Search does not
| Identity verification fails | Verify the email used by your Sim account in GitHub's email settings, then reconnect. A public profile email alone is insufficient. |
| Authorization fails after installation | Return to Sim and start **Connect** there. Do not enable authorization during installation. |
| Account authorization did not complete | Start the connection again from Sim. If it repeats, contact your organization admin or Sim support. For self-hosted Sim, check the [App callback and credentials](/platform/self-hosting/integrations-oauth#github-search). |
-| GitHub asks for a provider configuration update | An organization admin selects **Settings → Sources → More → Refresh connection settings**, then affected users reconnect GitHub. |
+| GitHub asks for a provider configuration update | An organization admin selects **Settings → Sources → More → Update sign-in settings**, then affected users reconnect GitHub. |
| Indexed files no longer appear | Confirm your own repository access, App repository selection, and connection status. Installation-indexed content is also withheld when GitHub cannot verify current access; retry once GitHub is available. |
| Sync is incomplete | Review the source status. Very large Git trees, file size limits, and unreadable files can limit indexing. |
| Empty repository returns an error | Add an initial commit, then sync again. GitHub does not return a file tree for an uninitialized repository. |
diff --git a/apps/docs/content/docs/search/gitlab.mdx b/apps/docs/content/docs/search/gitlab.mdx
index 39fb464de65..45f91816db9 100644
--- a/apps/docs/content/docs/search/gitlab.mdx
+++ b/apps/docs/content/docs/search/gitlab.mdx
@@ -9,8 +9,6 @@ import { Image } from '@/components/ui/image'
GitLab Search uses an administrator connection to sync a project's content and permissions. Teammates do not connect individual GitLab accounts. They sign in to the Sim organization with a verified email matching their confirmed primary GitLab email.
-
-
Admin setup uses your organization's **Settings → Sources** page. Teammates do not need a personal GitLab connection. For workspace Search, use **Search → Add source** instead.
## Before you start
@@ -49,12 +47,12 @@ The token must read the project, users, inherited project membership, instance s
### Configure the source in Sim
-Open **Settings → Sources** and turn on **GitLab**. Select **Set up** (or **Manage** if sources already exist), then **Add source**. Paste the **Personal Access Token**, enter your **Host** and **Project**, and choose the content to index.
+Open **Settings → Sources → Add source** and select **GitLab**. This opens **Add GitLab project**. Add one project per connection; to add another, open **GitLab** from the Sources list and select **Add project**. Paste the **Personal Access Token**, enter your **Host** and **Project**, and choose the content to index.
| Field | What to enter |
|---|---|
| Host | Your self-managed domain, such as `gitlab.example.com`. |
-| Project | `group/project` or the numeric project ID. Add another source for another project. |
+| Project | `group/project` or the numeric project ID. Use **Add project** for another project. |
| Content | Defaults to **Wiki & Issues**. Choose **Code, Wiki, Issues & Merge Requests** to include all supported types. |
| Branch | Optional branch or tag for repository files; blank uses the project's default branch. |
| Path Filter / File Extensions | Optional limits for repository files. |
@@ -63,8 +61,6 @@ Open **Settings → Sources** and turn on **GitLab**. Select **Set up** (or **Ma
**More options** contains the repository and issue filters, **Max Items**, and **Metadata tags**. Unlike member-account sources, GitLab retains this optional item limit.
-
-
Select **Connect & Sync**. Sim validates the token and source policy, then starts indexing.
@@ -74,7 +70,7 @@ Select **Connect & Sync**. Sim validates the token and source policy, then start
Invite teammates to the Sim organization using their verified work email. Sim matches that email against their confirmed primary GitLab email and applies project, feature, and confidential-issue permissions. Confirmed secondary addresses are not matched. No GitLab **Connect** step is required.
-Admins open **Settings → Sources**, select **Manage** beside **GitLab**, then open the source to inspect **Documents**, edit **Settings**, or review **Sync history**. GitLab has no personal **Accounts** tab. Permission and membership changes are picked up during background refreshes.
+Admins open **Settings → Sources**, open **GitLab**, then open the source to inspect **Documents**, edit **Settings**, or review **Sync history**. Permission and membership changes are picked up during background refreshes.
diff --git a/apps/docs/content/docs/search/gmail.mdx b/apps/docs/content/docs/search/gmail.mdx
index b4ce9e2e53f..990e806d51f 100644
--- a/apps/docs/content/docs/search/gmail.mdx
+++ b/apps/docs/content/docs/search/gmail.mdx
@@ -9,7 +9,7 @@ import { Image } from '@/components/ui/image'
Search email threads from your Gmail account. Members can connect their own accounts, or an administrator can index Google Workspace mailboxes with a service account. In either case, each mailbox stays private to its owner.
-Admin setup uses your organization's **Settings → Sources** page. Teammates connect from **Integrations** in the main sidebar. For workspace Search, use **Search → Add source** instead; **Create & Invite** is the workspace equivalent of **Add source**.
+Admin setup uses your organization's **Settings → Sources** page. Teammates connect from **Integrations** in the main sidebar. For workspace Search, use **Search → Add source** instead.
## Choose your setup
@@ -29,34 +29,34 @@ Central indexing does not make email searchable by the administrator, other reci
### Allow Gmail
-Open **Settings → Sources** and turn on **Gmail**. This allows personal connections; it does not connect anyone's account.
+An admin selects **Settings → Sources → Add source → Gmail**, chooses **Sync using → Member accounts**, sets any filters, and selects **Set up member accounts**.
### Connect your account
-Join the Sim organization, then open **Integrations** and select **Connect** beside Gmail. Authorize the Google account matching your verified Sim email. The first connection creates the default sync configuration: the last 6 months across all labels, excluding Promotions, Social, Spam, and Trash. Return to Integrations to see indexing status and your searchable document count.
+Join the Sim organization, then open **Integrations** and select **Connect** beside Gmail. Authorize the Google account matching your verified Sim email. The default filters cover the last 6 months across all labels, excluding Promotions, Social, Spam, and Trash. Return to Integrations to see indexing status and your searchable document count.
### Adjust filters if needed
-An admin selects **Manage** beside Gmail in **Settings → Sources**, opens **Advanced**, then selects the configuration's **Settings** tab. Change **Labels**, **Date Range**, or other filters and save.
+An admin opens **Settings → Sources → Gmail**, selects the connection, then selects the connection's **Settings** tab. Change **Labels**, **Date Range**, or other filters and save.
-One member-account configuration is usually enough. Its filters apply to all active Gmail connections, including accounts connected later. It does not assign different filters to selected people or let teammates search each other's mail. **Add sync configuration** opens the [central service-account setup](#set-up-a-central-service-account); it does not edit this member configuration.
+One member-account connection is usually enough. Its filters apply to all active Gmail connections, including accounts connected later. It does not assign different filters to selected people or let teammates search each other's mail. **Connect service account** opens the [central service-account setup](#set-up-a-central-service-account); it does not edit this member connection.
-Configurations are additive: a narrower one does not restrict an existing broader one, and overlapping configurations can index the same thread more than once. For one organization-wide policy, edit the existing configuration.
+Connections are additive: a narrower one does not restrict an existing broader one, and overlapping connections can index the same thread more than once. For one organization-wide policy, edit the existing connection.
-Admins can request member connections from **Manage → Accounts → Request connections**. These requests do not invite recipients to the Sim organization. See [Connect your account](/search/connect-your-account) for the shared connection and recovery steps.
+Admins can request member connections from **Settings → Sources → People → Request connections** (filter by the integration first). These requests do not invite recipients to the Sim organization. See [Connect your account](/search/connect-your-account) for the shared connection and recovery steps.
## Set up a central service account
-Open **Settings → Sources**, enable **Gmail**, and select **Manage → Advanced → Add sync configuration**. If personal connections are disabled for your organization, select **Add source** from the provider page instead.
+Open **Settings → Sources → Add source** and select **Gmail**. This opens **Connect Gmail service account**. To add another connection later, open **Gmail** from the Sources list and select **Connect service account**.
This requires Google Workspace and a Workspace super administrator to authorize delegation. Consumer Gmail accounts cannot use this path. Each selected user must have Gmail enabled.
@@ -91,7 +91,7 @@ If your organization requires multi-party approval, another super administrator
### Configure Gmail in Sim
-Under **Indexing account**, select **Add service account** and paste the JSON key into **Add Google Service Account**, or select an existing service account. Set **Directory administrator email** to an active Workspace administrator who can read users in the Directory API. This account supplies directory access; each mailbox is read using that mailbox owner's delegated identity.
+Under **Service account**, select **Add service account** and paste the JSON key into **Add Google Service Account**, or select an existing service account. Set **Directory administrator email** to an active Workspace administrator who can read users in the Directory API. This account supplies directory access; each mailbox is read using that mailbox owner's delegated identity.
Keep the default **Date Range** of **Last 6 months**, or adjust it and **Labels**. Use label names or system IDs such as `INBOX`; custom `Label_…` IDs are mailbox-specific. A label that does not exist in one mailbox simply matches no threads there.
@@ -104,9 +104,9 @@ Teammates join the Sim organization with their matching verified primary email.
## Source options
-An admin opens **Settings → Sources** and selects **Manage** beside **Gmail** to open its configuration list. Each row shows **Member accounts** or **Service account** beside its sync status. Open a configuration's **Settings** tab to edit its filters, then select **Save**. Filters apply separately to each mailbox in the source. **Documents** shows indexed threads and **Sync history** shows recent runs.
+An admin opens **Settings → Sources → Gmail** to open its configuration list. Each row shows **Member accounts** or **Service account** beside its sync status. Open a connection's **Settings** tab to edit its filters, then select **Save**. Filters apply separately to each mailbox in the connection. **Documents** shows indexed threads and **Sync history** shows recent runs.
-**Sync using** identifies the configuration's fixed connection method. To replace a central credential, choose another **Indexing account** and select **Change indexing account**.
+**Sync using** identifies the configuration's fixed connection method. To replace a central credential, choose another **Service account** and select **Change service account**.
| Option | Behavior |
| --- | --- |
@@ -155,7 +155,7 @@ Member-account connections use the deployment's Google OAuth client below. Centr
1. In [Google Cloud Console](https://console.cloud.google.com/), select your project. Open **APIs & Services → Library**, find **Gmail API**, and enable it.
2. Open **Google Auth platform → Branding**. Select **Get started** if needed, then enter the app name, support email, and contact email. Under **Audience**, use **Internal** only for an app limited to your Google Workspace organization; otherwise use **External** and add test users while testing. Review the app's permissions under **Data Access → Add or remove scopes**, using the current Sim scopes below. Follow Google's [consent and verification guidance](https://developers.google.com/workspace/guides/configure-oauth-consent) for your audience.
3. Open **Google Auth platform → Clients → Create client**. Choose **Web application**, give the client a name, and add the URI below under **Authorized redirect URIs**. If this instance already has a Google client, add this URI to that client instead. See [Google's credential setup](https://developers.google.com/workspace/guides/create-credentials#web-application).
-4. Save the client ID and secret as `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET`. Set `NEXT_PUBLIC_APP_URL` to the same Sim origin used in the callback, then restart Sim. See [Integrations & OAuth](/platform/self-hosting/integrations-oauth). If you change an existing deployment's OAuth client or scopes, an organization admin selects **Settings → Sources → More → Refresh connection settings**, then affected teammates reconnect.
+4. Save the client ID and secret as `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET`. Set `NEXT_PUBLIC_APP_URL` to the same Sim origin used in the callback, then restart Sim. See [Integrations & OAuth](/platform/self-hosting/integrations-oauth). If you change an existing deployment's OAuth client or scopes, an organization admin selects **Settings → Sources → More → Update sign-in settings**, then affected teammates reconnect.
```text
https:///api/auth/oauth2/callback/google-email
diff --git a/apps/docs/content/docs/search/google-calendar.mdx b/apps/docs/content/docs/search/google-calendar.mdx
index 37f427a369e..e9ff7efbbf6 100644
--- a/apps/docs/content/docs/search/google-calendar.mdx
+++ b/apps/docs/content/docs/search/google-calendar.mdx
@@ -9,7 +9,7 @@ import { Image } from '@/components/ui/image'
Search meetings and event details available to your Google account. Members can connect personal accounts, or a Google Workspace administrator can configure a central service-account crawl. Both paths keep each person's own view of events separate.
-Admin setup uses your organization's **Settings → Sources** page. Teammates connect from **Integrations** in the main sidebar. For workspace Search, use **Search → Add source** instead; **Create & Invite** is the workspace equivalent of **Add source**.
+Admin setup uses your organization's **Settings → Sources** page. Teammates connect from **Integrations** in the main sidebar. For workspace Search, use **Search → Add source** instead.
## Choose your setup
@@ -29,28 +29,28 @@ These are alternative setup paths. When only a central Calendar source is config
### Allow and connect Google Calendar
-An admin opens **Settings → Sources** and turns on **Google Calendar**. Each person joins the Sim organization, opens **Integrations**, selects **Connect** beside Google Calendar, and authorizes the Google account matching their verified Sim email. The first connection creates the default member-account sync configuration. Return to Integrations to see indexing status and your searchable document count.
+An admin selects **Settings → Sources → Add source → Google Calendar**, chooses **Sync using → Member accounts**, sets any filters, and selects **Set up member accounts**. Each person joins the Sim organization, opens **Integrations**, selects **Connect** beside Google Calendar, and authorizes the Google account matching their verified Sim email. Return to Integrations to see indexing status and your searchable document count.
### Choose calendars if needed
-An admin selects **Manage** beside Google Calendar in **Settings → Sources**, opens **Advanced**, and selects the configuration's **Settings** tab. Leave **Calendars** empty to search each person's primary calendar. To include specific shared calendars, select an **Account for browsing** and choose calendars, or switch to **Calendar IDs** and enter their IDs.
+An admin opens **Settings → Sources → Google Calendar**, selects the connection, then opens **Settings**. Leave **Calendars** empty to search each person's primary calendar. To include specific shared calendars, select an **Account for browsing** and choose calendars, or switch to **Calendar IDs** and enter their IDs.
**Account for browsing** only helps you choose calendars. It does not connect your account for Search or grant teammates access.
-### Save the configuration
+### Save the connection
-The default date range covers the previous and next 30 days. Save any changes to the existing configuration. Its calendar and date filters apply separately to each connected member's access. **Add sync configuration** opens the [central service-account setup](#set-up-a-central-service-account); it does not edit this member configuration.
+The default date range covers the previous and next 30 days. Save any changes to the existing connection. Its calendar and date filters apply separately to each connected member's access. **Connect service account** opens the [central service-account setup](#set-up-a-central-service-account); it does not edit this member connection.
-Admins can request member connections from **Manage → Accounts → Request connections**. These requests do not grant organization membership. Connecting Gmail or Drive does not replace the Calendar connection. See [Connect your account](/search/connect-your-account) for the shared connection and recovery steps.
+Admins can request member connections from **Settings → Sources → People → Request connections** (filter by the integration first). These requests do not grant organization membership. Connecting Gmail or Drive does not replace the Calendar connection. See [Connect your account](/search/connect-your-account) for the shared connection and recovery steps.
`primary` means the connected or impersonated person's main calendar. A calendar selected from the list is a specific calendar ID, even when it is your main calendar. That same ID applies to every selected user, and only users with access to it can search its events.
@@ -58,7 +58,7 @@ Admins can request member connections from **Manage → Accounts → Request con
## Set up a central service account
-Open **Settings → Sources** and turn on **Google Calendar**. Select **Manage → Advanced → Add sync configuration** to open the central service-account form. If personal connections are disabled for your organization, select **Add source** from the provider page instead.
+Open **Settings → Sources → Add source** and select **Google Calendar**. This opens **Connect Google Calendar service account**. To add another connection later, open **Google Calendar** from the Sources list and select **Connect service account**.
This requires a Google Workspace customer and a super administrator to authorize domain-wide delegation. Consumer Gmail accounts cannot use this path.
@@ -93,7 +93,7 @@ The **Directory administrator email** must be an active Workspace administrator
### Add the credential and choose users
-Under **Indexing account**, select **Add service account** or an existing service account. In **Add Google Service Account**, give the credential a name and paste its JSON key. Back in the source form, enter the **Directory administrator email**.
+Under **Service account**, select **Add service account** or an existing service account. In **Add Google Service Account**, give the credential a name and paste its JSON key. Back in the source form, enter the **Directory administrator email**.
Leave **Calendar IDs** empty for each user's `primary` calendar. To include shared calendars, enter their IDs, optionally alongside `primary`. IDs apply to each selected user who can read that calendar; this does not share calendars or expand anyone's Google access. Central setup uses manual IDs because an administrator's calendar picker would not represent every user's calendars.
@@ -106,9 +106,9 @@ Sim verifies Directory access and selected users, then probes one selected user'
## Source options
-An admin opens **Settings → Sources** and selects **Manage** beside **Google Calendar** to open its configuration list. Each row shows **Member accounts** or **Service account** beside its sync status. Open a configuration's **Settings** tab to edit its filters, then select **Save**. **Documents** shows indexed events and **Sync history** shows recent runs.
+An admin opens **Settings → Sources → Google Calendar** to open its configuration list. Each row shows **Member accounts** or **Service account** beside its sync status. Open a connection's **Settings** tab to edit its filters, then select **Save**. **Documents** shows indexed events and **Sync history** shows recent runs.
-**Sync using** identifies the configuration's fixed connection method. To replace a central credential, choose another **Indexing account** and select **Change indexing account**.
+**Sync using** identifies the configuration's fixed connection method. To replace a central credential, choose another **Service account** and select **Change service account**.
| Option | Behavior |
| --- | --- |
@@ -153,7 +153,7 @@ The deployment operator configures Google OAuth for member accounts and the memb
1. In [Google Cloud Console](https://console.cloud.google.com/), select your project. Open **APIs & Services → Library**, find **Google Calendar API**, and enable it.
2. Open **Google Auth platform → Branding** and configure the app name and contact details. Under **Audience**, choose **Internal** for your Google Workspace organization only, or **External** for other users. Add test users while an external app is testing. Review **Data Access → Add or remove scopes** using the current Sim scopes below. See Google's [consent and verification guidance](https://developers.google.com/workspace/guides/configure-oauth-consent).
3. Open **Google Auth platform → Clients → Create client**, choose **Web application**, and add the URI below under **Authorized redirect URIs**. Add it to the existing Google client if the instance already uses one. See [Google's credential setup](https://developers.google.com/workspace/guides/create-credentials#web-application).
-4. Save the client ID and secret as `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET`. Set `NEXT_PUBLIC_APP_URL` to the same Sim origin used in the callback, then restart Sim. See [Integrations & OAuth](/platform/self-hosting/integrations-oauth). If you change an existing deployment's OAuth client or scopes, an organization admin selects **Settings → Sources → More → Refresh connection settings**, then affected teammates reconnect.
+4. Save the client ID and secret as `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET`. Set `NEXT_PUBLIC_APP_URL` to the same Sim origin used in the callback, then restart Sim. See [Integrations & OAuth](/platform/self-hosting/integrations-oauth). If you change an existing deployment's OAuth client or scopes, an organization admin selects **Settings → Sources → More → Update sign-in settings**, then affected teammates reconnect.
```text
https:///api/auth/oauth2/callback/google-calendar
diff --git a/apps/docs/content/docs/search/google-drive.mdx b/apps/docs/content/docs/search/google-drive.mdx
index 52ecfc4864b..0ba9cb742a2 100644
--- a/apps/docs/content/docs/search/google-drive.mdx
+++ b/apps/docs/content/docs/search/google-drive.mdx
@@ -9,7 +9,7 @@ import { Image } from '@/components/ui/image'
Search Google Docs, Sheets, Slides, and supported files in Drive. Members can connect their own accounts after an admin allows Google Drive, or an admin can configure a central service-account crawl.
-Admin setup uses your organization's **Settings → Sources** page. Teammates connect from **Integrations** in the main sidebar. For workspace Search, use **Search → Add source** instead; **Create & Invite** is the workspace equivalent of **Add source**.
+Admin setup uses your organization's **Settings → Sources** page. Teammates connect from **Integrations** in the main sidebar. For workspace Search, use **Search → Add source** instead.
## Choose your setup
@@ -31,36 +31,36 @@ These are alternative setup paths. When only a central Drive source is configure
### Allow Google Drive
-An organization admin opens **Settings → Sources** and turns on **Google Drive**. This allows personal connections; it does not create a source or connect anyone's account.
+An admin selects **Settings → Sources → Add source → Google Drive**, chooses **Sync using → Member accounts**, sets any filters, and selects **Set up member accounts**.
### Connect your account
-Join the Sim organization, then open **Integrations** and select **Connect** beside Google Drive. Use the Google account matching your verified Sim email. The first personal connection can create a source with default filters. Return to Integrations to see indexing status and your searchable document count.
+Join the Sim organization, then open **Integrations** and select **Connect** beside Google Drive. Use the Google account matching your verified Sim email. Return to Integrations to see indexing status and your searchable document count.
### Adjust filters if needed
-An admin opens **Settings → Sources**, selects **Manage** beside **Google Drive**, opens **Advanced**, and selects the sync configuration's **Settings** tab. Leave **Folders** empty to include supported files each member can access, or narrow the source to folders. **Account for browsing** helps select folders; manual **Folder IDs** work without it. Browsing does not connect that account to Search.
+An admin opens **Settings → Sources → Google Drive**, selects the connection, then opens **Settings**. Leave **Folders** empty to include supported files each member can access, or narrow the source to folders. **Account for browsing** helps select folders; manual **Folder IDs** work without it. Browsing does not connect that account to Search.
Keep **Sync documents with → Connected members** unless a dedicated account should fetch content. Members still connect to establish access. If the dedicated account is a delegated service account, **Crawl as** selects the Google Workspace user whose files it fetches. Save the source settings when finished.
-Admins can request member connections from **Manage → Accounts → Request connections**. These requests do not grant organization membership. See [Connect your account](/search/connect-your-account) for the shared connection and recovery steps.
+Admins can request member connections from **Settings → Sources → People → Request connections** (filter by the integration first). These requests do not grant organization membership. See [Connect your account](/search/connect-your-account) for the shared connection and recovery steps.
## Set up a central service account
-Open **Settings → Sources** and turn on **Google Drive**. Select **Manage → Advanced → Add sync configuration** to open the central service-account form directly. If personal connections are disabled for your organization, select **Add source** from the provider page instead. Teammates do not need a personal Drive connection for this source.
+Open **Settings → Sources → Add source** and select **Google Drive**. This opens **Connect Google Drive service account**. To add another connection later, open **Google Drive** from the Sources list and select **Connect service account**.
This requires a Google Workspace domain and a Workspace super administrator to authorize domain-wide delegation. Consumer Gmail accounts cannot use this path.
-
+
@@ -101,7 +101,7 @@ Group permissions require groups and memberships that this administrator can rea
### Add the credential in Sim
-Under **Indexing account**, choose **Add service account**, or select an existing service account. Paste the JSON key into **Add Google Service Account**, give it a name, and add it. Sim returns you to the source form with that credential selected.
+Under **Service account**, choose **Add service account**, or select an existing service account. Paste the JSON key into **Add Google Service Account**, give it a name, and add it. Sim returns you to the source form with that credential selected.
@@ -112,7 +112,7 @@ Under **Indexing account**, choose **Add service account**, or select an existin
Set **Directory administrator email** to the Workspace administrator described above. Under **More options**, leave **Users** blank for everyone, or enter up to 100 primary Workspace email addresses separated by commas. Suspended, archived, and guest accounts are excluded.
-Leave **Folders** empty to include supported files each selected user can access. To narrow the source, select folders visible to the Directory administrator or enter **Folder IDs** manually. The same folder filter applies to each selected user and does not grant access. Choose **Connect & Sync**. Sim validates the administrator and any selected users, and probes Drive access for an active user, before accepting the source.
+Leave **Folders** empty to include supported files each selected user can access. To narrow the source, select folders visible to the Directory administrator or enter **Folder IDs** manually. The same folder filter applies to each selected user and does not grant access. Choose **Connect & Sync**. Sim validates the administrator and any selected users, and probes Drive access for an active user, before accepting the connection.
The crawl pages through each user’s files and shared drives, including shared-drive files the user has never opened. It resumes unfinished work and indexes a shared file once even when several users can access it. A file must be downloadable by at least one selected user to be indexed; the same requirement applies to a shortcut’s target. Sim must also verify its permissions before showing it in Search. Google can let a reader download a file while refusing to list its permissions; include an owner or another user who can read those permissions. Externally owned files can remain hidden when no selected user can verify their permissions.
@@ -123,9 +123,9 @@ Teammates join the Sim organization with their matching verified email; they do
## Source options
-An admin opens **Settings → Sources** and selects **Manage** beside **Google Drive** to open its configuration list. Each row shows **Member accounts** or **Service account** beside its sync status. Open a configuration's **Settings** tab to edit its filters, then select **Save**. **Documents** shows indexed files and **Sync history** shows recent runs.
+An admin opens **Settings → Sources → Google Drive** to open its configuration list. Each row shows **Member accounts** or **Service account** beside its sync status. Open a connection's **Settings** tab to edit its filters, then select **Save**. **Documents** shows indexed files and **Sync history** shows recent runs.
-**Sync using** identifies the configuration's fixed connection method. To replace a central credential, choose another **Indexing account** and select **Change indexing account**.
+**Sync using** identifies the configuration's fixed connection method. To replace a central credential, choose another **Service account** and select **Change service account**.
| Option | Behavior |
| --- | --- |
@@ -139,7 +139,7 @@ An admin opens **Settings → Sources** and selects **Manage** beside **Google D
Sim exports Docs and Slides as text and Sheets as XLSX spreadsheets. Supported uploaded files use the knowledge-base document pipeline, including PDF and Office formats. Unsupported files and oversized exports cannot be indexed; Google limits Workspace exports to 10 MB. See [Drive export formats](https://developers.google.com/workspace/drive/api/guides/ref-export-formats) and [download limits](https://developers.google.com/workspace/drive/api/guides/manage-downloads).
-Search schedules syncs hourly. Central crawls revisit the selected users' files and permissions, including unchanged files, so permission changes and a new employee's older files are included. Unfinished crawls resume before deletion reconciliation. Content, deletions, and permissions refresh in the background; results are not a live read from Drive. **Accounts** on the provider page shows personal account connections where configured; it does not list the central service-account credential.
+Search schedules syncs hourly. Central crawls revisit the selected users' files and permissions, including unchanged files, so permission changes and a new employee's older files are included. Unfinished crawls resume before deletion reconciliation. Content, deletions, and permissions refresh in the background; results are not a live read from Drive. **People** on the Sources settings page shows personal account connections; it does not list the central service-account credential.
## Troubleshooting
@@ -168,7 +168,7 @@ https:///api/auth/oauth2/callback/google-drive
This Google Cloud example uses one client for all three services. Replace `https://sim.example.com` with your Sim origin and add only the callbacks for services you enable.
-Save the client ID and secret as `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET`. Set `NEXT_PUBLIC_APP_URL` to the same Sim origin used in the callback, then restart Sim. See [Integrations & OAuth](/platform/self-hosting/integrations-oauth). If you change an existing deployment's OAuth client or scopes, an organization admin selects **Settings → Sources → More → Refresh connection settings**, then affected teammates reconnect.
+Save the client ID and secret as `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET`. Set `NEXT_PUBLIC_APP_URL` to the same Sim origin used in the callback, then restart Sim. See [Integrations & OAuth](/platform/self-hosting/integrations-oauth). If you change an existing deployment's OAuth client or scopes, an organization admin selects **Settings → Sources → More → Update sign-in settings**, then affected teammates reconnect.
The current Sim Drive OAuth connection uses these scopes:
diff --git a/apps/docs/content/docs/search/index.mdx b/apps/docs/content/docs/search/index.mdx
index b3b9aa797be..f9203ead4e1 100644
--- a/apps/docs/content/docs/search/index.mdx
+++ b/apps/docs/content/docs/search/index.mdx
@@ -14,22 +14,18 @@ Search brings your connected sources into one place. An organization admin allow
-### Allow an integration
+### Choose an integration
-As an organization admin, open **Settings → Sources** and turn on the integration. It stays in the list; no setup page opens automatically.
-
-The switch saves immediately and permits the integration in your organization. It does not connect an account, grant document access, or start indexing. Teammates connect from **Integrations** in the main sidebar; only admins manage these switches.
+Open **Settings → Sources → Add source** and choose an integration. Unavailable integrations need deployment configuration before they can be connected.
-### Connect or configure the source
-
-For personal Gmail, Calendar, or Drive, teammates can connect immediately from **Integrations**. The first account creates the default sync configuration. Admins can adjust it later under **Manage → Advanced** on the provider page.
+### Connect the content
-For other sources, select **Set up** or **Manage**, then **Add source**. For a central Gmail, Calendar, or Drive service account, use **Manage → Advanced → Add sync configuration**. For Slack, complete **Set up Slack app** first. Follow the provider's **Setup guide** to configure its connection and content. **More options** contains secondary filters and **Metadata tags**.
+Follow the integration's **Setup guide**. Confluence connects a site and selected spaces; Jira adds projects; GitHub adds a repository; GitLab adds a project; Slack configures its app, then channels or DMs. Google service-account setup connects Drive, Gmail, or Calendar with the selected users and filters.
-In the source form, select **Connect & Sync** for a central account or **Add source** for member accounts. Creating a source does not invite people or authorize their accounts.
+Select **Connect & Sync** for a central account, or the form’s **Add** or **Set up member accounts** action for member accounts. **More options** contains additional filters. Saving a connection does not invite teammates or authorize their accounts.
@@ -41,9 +37,9 @@ For member-account sources, open **Integrations** in the main sidebar and select
-
+
-Source availability depends on the deployment and organization policy. An unavailable source needs operator configuration before setup can continue. If an existing provider asks for a configuration update after a deployment change, an admin selects **More → Refresh connection settings** on this page, then affected teammates reconnect.
+Source availability depends on the deployment and organization policy. An unavailable source needs operator configuration before setup can continue. If an existing provider asks for a configuration update after a deployment change, an admin selects **More → Update sign-in settings** on this page, then affected teammates reconnect.
## Choose the right connection method
@@ -57,7 +53,9 @@ Sources can use member accounts or a central connection. Gmail, Google Calendar,
| **Confluence service account** | Connects an account that can read the selected content and its permissions. | Join the organization and connect Confluence to establish identity. |
| **Administrator token** (GitLab) | Connects a self-managed instance administrator token and selects projects to index. | Join the organization with a verified Sim email matching GitLab. No personal connection is needed. |
-Adding a Google or Confluence source from the admin page opens its central setup form directly. Google providers use **Manage → Advanced → Add sync configuration**; Confluence uses **Set up/Manage → Add source**. For personal connections, use **Integrations → Connect** in the main sidebar. An approved provider can create its first member source there; required repository, site, or project fields are collected before authorization. Admins can edit that source's filters afterward in its **Settings** tab. When personal connections are disabled for the organization, Google providers use **Add source** on the provider page instead of **Advanced**.
+Open an integration from the Sources list to manage its connections. Use **Connect service account** for Google, **Add Confluence site**, **Add projects** for Jira, **Add repository** for GitHub, **Add project** for GitLab, or **Add channels or DMs** for Slack.
+
+For personal connections, teammates use **Integrations → Connect** in the main sidebar after the admin adds the provider. First-time setup may ask you to select content after connecting your account. Admins can edit those connections' filters afterward in **Settings**.
Some member sources offer **Sync documents with**, either directly in setup or under **More options**. **Connected members** uses members' accounts for both content and access checks. Selecting a dedicated account uses it to fetch content; members still connect to establish which documents they may search. For GitHub organization sources, choose **Connect GitHub App** in this field to [connect an installation](/search/github#add-a-repository). **Account for browsing** only helps an admin pick source options—it does not enroll that account for Search.
@@ -82,9 +80,9 @@ Some member sources offer **Sync documents with**, either directly in setup or u
Invite people through the organization's **Settings → Members**, or use your organization's [SSO provisioning](/platform/enterprise/sso). Share the organization's **Home** or **Integrations** URL. People need their own Sim account and organization membership; they do not need access to a workspace. Connecting an external account alone does not grant organization membership.
-To request a particular account connection, open **Settings → Sources**, select **Manage** beside the integration (or **Set up** if no sources exist), then **Accounts → Request connections**. This tab is available for providers that support personal connections. If setup is missing, complete the source or Slack app setup shown there first. The list shows organization account contributors and their connection status for that provider.
+Open **Settings → Sources → People** to view account contributors and their connection status. Choose an integration in the filter to inspect its connections or send a targeted **Request connections** invitation. **All integrations** shows each person's connected providers and accounts needing reconnection.
-These requests are separate from organization invitations. They let recipients contribute an external account; they do not grant Sim organization membership or Search access. **People** on the Sources settings page shows account contributors across providers. See [Connect your account](/search/connect-your-account) for the teammate walkthrough.
+Connection requests do not grant organization membership. See [Connect your account](/search/connect-your-account) for the teammate walkthrough.
**Revoke all account access** withdraws every account that person contributed to the organization, including other providers. It does not remove their organization membership.
@@ -92,26 +90,28 @@ These requests are separate from organization invitations. They let recipients c
## Manage sources and documents
-Open **Settings → Sources** and select **Manage** beside the integration. When personal connections are enabled, Gmail, Calendar, and Drive list their sync configurations under **Advanced** and connections under **Accounts**. Other providers with personal connections use **Sources** and **Accounts**. Providers without personal connections open directly to the source list. Select a source or sync configuration to manage it. Multiple sources can have different folder, repository, space, or project scopes.
+Open **Settings → Sources**, choose the integration, then select its site, project, repository, or account connection. Each row identifies **Member accounts**, **Service account**, or **Admin account** and shows its sync status.
-For Gmail, one configuration is usually enough. **Add sync configuration** creates another source; editing **Settings** updates the selected source. A member configuration applies to all connected Gmail accounts, including accounts connected later. A central configuration applies to the selected Workspace **Users**, or all active users when that field is blank. Its labels and filters are evaluated separately in each mailbox.
+Edit **Settings** to change an existing connection's filters. Adding another connection creates a separate selection. For Gmail, one member-account connection is usually enough: its filters apply separately to each connected mailbox, including accounts connected later. A central Google connection applies to its selected **Users**, or all active users when that field is blank.
-
+
-| Source tab | What you can do |
+| Connection tab | What you can do |
| --- | --- |
| **Documents** | Find indexed documents, inspect processing status, retry failed indexing, or exclude and restore documents. |
| **Settings** | Edit the source's scope, filters, and supported indexing credentials. Select **Save** to apply changes or **Discard** to undo them. |
| **Sync history** | Review run dates, document changes, and any sync or account errors. |
-**Sync using** shows the method selected when the source was created. Create a new source to change that method. To replace a supported indexing credential, select its replacement and use **Change indexing account**.
+**Sync using** shows the method selected when the source was created. Add a new connection to change that method. To replace a supported indexing credential, select its replacement and use **Change service account** or **Change account**, as shown.
-Use the source header to sync, pause, resume, or remove that source. The back link returns to its integration. To disable an entire integration, turn off its switch in **Settings → Sources**. If it has sources, confirm **Deactivate**. Its content becomes unavailable in Search, Assistant, and MCP; sources and connected accounts are preserved. Turn the switch back on to allow it again.
-
-
+Use the connection header to sync, pause, resume, or remove it. To deactivate an entire integration, open it from **Settings → Sources**, select **Deactivate**, and confirm. Its content becomes unavailable in Search, Assistant, and MCP; saved connections remain. Select **Activate** on that integration to enable it again.
+## Usage stats
+
+**Settings → Sources → Stats** shows successful Search invocations, active people, and results returned. Filter by entry point and a preset or custom date range of up to 90 days. Dates use UTC. Source usage counts requests that returned content from each integration; it is not a click or citation count. Tracking starts when enabled and does not backfill earlier activity.
+
## Search, Assistant, and MCP
**Search** in the organization sidebar finds documents directly. The assistant on **Home** can search and read the same sources to answer questions with citations. Conversations are private to their author, including when another organization member is an admin.
@@ -135,7 +135,7 @@ Search runs background syncs on an hourly schedule. Large sources, provider limi
A completed sync means the source was checked; some documents may still be indexing. Integrations lists personal connections; a central Google source can be searchable without appearing there. In the main **Integrations** page, each connected source row shows how many documents you can search and whether indexing failed for any documents you can access.
-As an admin, open **Settings → Sources**, select **Manage** beside the integration, then open its source or sync configuration. In **Documents**, select **Failed** from the status dropdown to inspect those files. Use the search field to find a document by name. Select **Retry indexing** beside a file to try again. **Exclude** removes a file from search; select **Excluded** and then **Restore** to include it again. Fix a disconnected account or source configuration before retrying a sync that needs attention.
+As an admin, open **Settings → Sources**, select the integration, then open its connection. In **Documents**, select **Failed** from the status dropdown to inspect those files. Use the search field to find a document by name. Select **Retry indexing** beside a file to try again. **Exclude** removes a file from search; select **Excluded** and then **Restore** to include it again. Fix a disconnected account or source configuration before retrying a sync that needs attention.
diff --git a/apps/docs/content/docs/search/jira.mdx b/apps/docs/content/docs/search/jira.mdx
index b793135c408..a50a7d6ffac 100644
--- a/apps/docs/content/docs/search/jira.mdx
+++ b/apps/docs/content/docs/search/jira.mdx
@@ -30,23 +30,18 @@ Search uses `read:jira-work`, `read:me`, and `offline_access` for issues, identi
### Choose Jira
-Open your organization's **Settings → Sources**, turn on **Jira**, then select **Set up** (or **Manage**) → **Add source**.
+Open **Settings → Sources → Add source** and select **Jira**. This opens **Add Jira projects**. To add another project selection later, open **Jira** from the Sources list and select **Add projects**.
### Choose the projects
-Enter **Jira Domain**. Under **Account for browsing**, select an account or choose **Connect Jira account**, then select **Projects**. To enter keys such as `ENG, SUPPORT` manually, use the switch beside Projects; this works without a browsing account.
+Under **Account for browsing**, select a saved account (including your personal Search account) or choose **Connect Jira account**. Enter **Jira site**, then choose **Projects**. **All** in the dropdown selects every project the account can currently browse; newly created projects are not added automatically. Clear the picker search before selecting all.
-**Account for browsing** only loads the project picker, using OAuth or **Add service account**. It does not enroll anyone for Search. A service account needs Jira access and the [Jira read scopes](/integrations/atlassian-service-account#jira-and-jira-service-management); Confluence-only tokens do not work here.
+To enter keys such as `ENG, SUPPORT` manually, use the switch beside **Projects**; this works without a browsing account. Switching between the picker and manual entry keeps your selection.
-
+**Account for browsing** only loads the project picker, using OAuth or **Add service account**. It does not enroll anyone for Search. A service account needs Jira access and the [Jira read scopes](/integrations/atlassian-service-account#jira-and-jira-service-management); Confluence-only tokens do not work here.
@@ -55,7 +50,7 @@ Enter **Jira Domain**. Under **Account for browsing**, select an account or choo
Under **More options**, optionally set a **JQL Filter**, such as `status = "Done"`, and choose **Metadata tags**. Leave JQL empty for all accessible issues in the selected projects. Enter conditions only; omit `ORDER BY` because Sim supplies sorting.
-Select **Add source**. This saves the shared scope; it does not authorize accounts or send invitations.
+Select **Add projects**. This saves the shared scope; it does not authorize accounts or send invitations.
@@ -71,13 +66,28 @@ Each teammate follows this step. Existing authorized accounts may already be con
For workspace Search, start from **Search → Add source**; its source-creation button is **Create & Invite**. Search manages the sync schedule and hides item limits.
-## Manage the source
+## Connect before a source exists
+
+After an admin approves Jira, a teammate can start from **Integrations → Connect**. The same setup opens when you choose **Connect a different site or content scope**:
+
+1. Under **Your account**, select a saved account or choose **Connect account**. Authorize using the Atlassian email matching your verified Sim email.
+2. Enter the hostname under **Atlassian site**, then choose **Projects**. Use **All** in the dropdown for the complete current list, or **Enter keys manually** for comma-separated keys. You can select up to 1,000 projects in this form.
+3. Select **Connect & Sync**. Sim saves the selected scope and starts indexing with your account.
+
+
+
+## Manage project connections
-Admins open **Settings → Sources → Jira → Manage**, then a source's **Documents**, **Settings**, or **Sync history**. Metadata tags include issue type, status, priority, labels, assignee, and last updated.
+Admins open **Settings → Sources → Jira**, then a connection’s **Documents**, **Settings**, or **Sync history**. Metadata tags include issue type, status, priority, labels, assignee, and last updated.
-Teammates share the configured scope and do not choose projects again. For another site or scope, add another source, or use **Connect** beside **Connect a different site or content scope** in Integrations.
+Teammates share the configured scope and do not choose projects again. For another site or scope, select **Add projects**, or use **Connect** beside **Connect a different site or content scope** in Integrations.
-Invite teammates through **Settings → Members → Invite** or SSO, then have them connect Jira through **Integrations**. **Jira → Accounts → Request connections** only requests a provider connection; it does not invite people to the organization.
+Invite teammates through **Settings → Members → Invite** or SSO, then have them connect Jira through **Integrations**. **Settings → Sources → People**, filter by **Jira**, then select **Request connections** only requests a provider connection; it does not invite people to the organization.
Sim checks Jira separately for each connected person. Content becomes searchable as indexing finishes; issue changes and lost access are reflected after later syncs.
@@ -104,6 +114,6 @@ Configure one shared Jira OAuth app for the deployment:
2. Under **Authorization → OAuth 2.0 (3LO)**, add `https:///api/auth/oauth2/callback/jira` as a callback.
3. Under **Permissions**, add **Jira API** and configure the full `jira` scope list from [Sim's OAuth configuration](https://github.com/simstudioai/sim/blob/staging/apps/sim/lib/oauth/oauth.ts), including its Jira Service Management and Assets scopes. Add **User Identity API → read:me**. Sim requests `offline_access` for refresh tokens; Search's three scopes above are only a subset of this shared app's permissions.
4. Enable sharing under **Distribution**. Set `JIRA_CLIENT_ID` and `JIRA_CLIENT_SECRET` from the app's **Settings**, verify `NEXT_PUBLIC_APP_URL`, and restart Sim.
-5. Connect from **Integrations** and select the configured site. After changing the OAuth client or requested scopes, use **Settings → Sources → More → Refresh connection settings**, then have affected teammates reconnect.
+5. Connect from **Integrations** and select the configured site. After changing the OAuth client or requested scopes, use **Settings → Sources → More → Update sign-in settings**, then have affected teammates reconnect.
The callback must match Sim's scheme, hostname, port, and path exactly. If only the app owner can connect, check **Distribution**. See the [Atlassian OAuth guide](https://developer.atlassian.com/cloud/jira/platform/oauth-2-3lo-apps/) and [Sim deployment reference](/platform/self-hosting/integrations-oauth).
diff --git a/apps/docs/content/docs/search/slack.mdx b/apps/docs/content/docs/search/slack.mdx
index 2b63bb88e82..11f17e1b687 100644
--- a/apps/docs/content/docs/search/slack.mdx
+++ b/apps/docs/content/docs/search/slack.mdx
@@ -23,7 +23,7 @@ Skip to **Connect the source** if the organization already has a verified Slack
### Open provider settings
-Open **Settings → Sources** and turn on **Slack**. Select **Set up** (or **Manage** if sources already exist), then **Set up Slack app**. Allowing Slack does not configure the app or start indexing.
+Open **Settings → Sources → Add source** and select **Slack**. Complete **Set up Slack app** before choosing messages to index. If Slack is already listed, open it and select **Set up Slack app**.
@@ -53,14 +53,14 @@ Return to **Settings → Sources → Slack → Set up Slack app**. Select **Veri
Member connections use one configured Slack app and workspace per organization. Installing another bot does not change that configuration. Changing the verified app requires members to reconnect.
-After verification, open **Sources → Add source** on Slack’s provider page to choose what to index. A connected bot alone does not mean Slack messages have been indexed.
+After verification, select **Add channels or DMs** on Slack’s provider page to choose what to index. A connected bot alone does not mean Slack messages have been indexed.
## Connect the source
-Open **Sources → Add source** on Slack's provider page. Choose **Channel Messages**, **Direct Messages**, **Channels**, and **Earliest Message Date**. Open **More options** for exclusions, archived channels, metadata tags, and **Sync documents with**. Keep **Connected members** for the usual setup.
+Open **Add channels or DMs** on Slack's provider page. Choose **Channel Messages**, **Direct Messages**, **Channels**, and **Earliest Message Date**. Open **More options** for exclusions, archived channels, metadata tags, and **Sync documents with**. Keep **Connected members** for the usual setup.
| Field | Behavior |
|---|---|
@@ -71,15 +71,15 @@ Open **Sources → Add source** on Slack's provider page. Choose **Channel Messa
| Archived Channels | Included by default. The picker lists active channels; use manual names/IDs to select archived channels. |
| Earliest Message Date | Optional UTC date (`YYYY-MM-DD`). Applies to the thread's first message; replies are included with that thread. |
-Select **Add source**. Each person opens **Integrations** in the main sidebar, selects **Connect** on the Slack source, and approves the configured app. Creating the source or verifying the Slack app does not authorize teammates automatically.
+Select **Add channels or DMs**. Each person opens **Integrations** in the main sidebar, selects **Connect** on the Slack source, and approves the configured app. Creating the source or verifying the Slack app does not authorize teammates automatically.
The Slack app's **Home → Connect sources** opens this same Integrations page.
-To send a Slack connection request, open **Slack → Accounts → Request connections**. This requests an external account connection; it does not invite the recipient to the Sim organization.
+To send a Slack connection request, open **Settings → Sources → People**, filter by **Slack**, then select **Request connections**. This requests an external account connection; it does not invite the recipient to the Sim organization.
You can instead select an existing account under **Sync documents with** to supply message content centrally. Members still connect their own accounts to establish access. The selected account must itself be able to read the selected channels.
-Admins open **Settings → Sources**, select **Manage** beside **Slack**, then open the source to inspect **Documents**, edit **Settings**, or review **Sync history**. Search reads indexed content, so source changes appear after background syncing. Slack retention and API limits determine how much history is available.
+Admins open **Settings → Sources**, open **Slack**, then open the source to inspect **Documents**, edit **Settings**, or review **Sync history**. Search reads indexed content, so source changes appear after background syncing. Slack retention and API limits determine how much history is available.
## Permissions reference
diff --git a/apps/docs/public/static/search/confluence-personal-setup.png b/apps/docs/public/static/search/confluence-personal-setup.png
new file mode 100644
index 00000000000..f8c4119d62c
Binary files /dev/null and b/apps/docs/public/static/search/confluence-personal-setup.png differ
diff --git a/apps/docs/public/static/search/confluence-setup.png b/apps/docs/public/static/search/confluence-setup.png
deleted file mode 100644
index 8ce6c182f59..00000000000
Binary files a/apps/docs/public/static/search/confluence-setup.png and /dev/null differ
diff --git a/apps/docs/public/static/search/github-indexing-options.jpg b/apps/docs/public/static/search/github-indexing-options.jpg
deleted file mode 100644
index ff4a800f808..00000000000
Binary files a/apps/docs/public/static/search/github-indexing-options.jpg and /dev/null differ
diff --git a/apps/docs/public/static/search/github-setup.jpg b/apps/docs/public/static/search/github-setup.jpg
deleted file mode 100644
index 454bd18b10e..00000000000
Binary files a/apps/docs/public/static/search/github-setup.jpg and /dev/null differ
diff --git a/apps/docs/public/static/search/gitlab-options.jpg b/apps/docs/public/static/search/gitlab-options.jpg
deleted file mode 100644
index c2ab696601f..00000000000
Binary files a/apps/docs/public/static/search/gitlab-options.jpg and /dev/null differ
diff --git a/apps/docs/public/static/search/gitlab-setup.jpg b/apps/docs/public/static/search/gitlab-setup.jpg
deleted file mode 100644
index eb2dccf93e7..00000000000
Binary files a/apps/docs/public/static/search/gitlab-setup.jpg and /dev/null differ
diff --git a/apps/docs/public/static/search/google-drive-setup.jpg b/apps/docs/public/static/search/google-drive-setup.jpg
deleted file mode 100644
index a61673d3495..00000000000
Binary files a/apps/docs/public/static/search/google-drive-setup.jpg and /dev/null differ
diff --git a/apps/docs/public/static/search/google-drive-setup.png b/apps/docs/public/static/search/google-drive-setup.png
new file mode 100644
index 00000000000..add1c737455
Binary files /dev/null and b/apps/docs/public/static/search/google-drive-setup.png differ
diff --git a/apps/docs/public/static/search/integration-provider.jpg b/apps/docs/public/static/search/integration-provider.jpg
deleted file mode 100644
index bf655343f51..00000000000
Binary files a/apps/docs/public/static/search/integration-provider.jpg and /dev/null differ
diff --git a/apps/docs/public/static/search/integration-provider.png b/apps/docs/public/static/search/integration-provider.png
new file mode 100644
index 00000000000..535a5860a03
Binary files /dev/null and b/apps/docs/public/static/search/integration-provider.png differ
diff --git a/apps/docs/public/static/search/integration-settings.jpg b/apps/docs/public/static/search/integration-settings.jpg
deleted file mode 100644
index f4e517c0656..00000000000
Binary files a/apps/docs/public/static/search/integration-settings.jpg and /dev/null differ
diff --git a/apps/docs/public/static/search/integration-settings.png b/apps/docs/public/static/search/integration-settings.png
new file mode 100644
index 00000000000..8dcffb53336
Binary files /dev/null and b/apps/docs/public/static/search/integration-settings.png differ
diff --git a/apps/docs/public/static/search/jira-personal-setup.png b/apps/docs/public/static/search/jira-personal-setup.png
new file mode 100644
index 00000000000..9530eb542d2
Binary files /dev/null and b/apps/docs/public/static/search/jira-personal-setup.png differ
diff --git a/apps/docs/public/static/search/jira-setup.png b/apps/docs/public/static/search/jira-setup.png
deleted file mode 100644
index 393b062c641..00000000000
Binary files a/apps/docs/public/static/search/jira-setup.png and /dev/null differ
diff --git a/apps/docs/public/static/search/source-settings.jpg b/apps/docs/public/static/search/source-settings.jpg
deleted file mode 100644
index d89c8062380..00000000000
Binary files a/apps/docs/public/static/search/source-settings.jpg and /dev/null differ
diff --git a/apps/sim/app/api/knowledge/sim-search/personal-source-setup/route.ts b/apps/sim/app/api/knowledge/sim-search/personal-source-setup/route.ts
new file mode 100644
index 00000000000..8fff7a8ff6e
--- /dev/null
+++ b/apps/sim/app/api/knowledge/sim-search/personal-source-setup/route.ts
@@ -0,0 +1,70 @@
+import {
+ listPersonalSourceSetupAccountsContract,
+ personalSourceSetupContract,
+} from '@/lib/api/contracts/knowledge/personal-source-setup'
+import {
+ defineInternalJsonRoute,
+ extendInternalErrorPolicy,
+ internalErrorResponse,
+ internalRateLimits,
+ internalSessionAuth,
+} from '@/lib/api/server/routes'
+import { internalKnowledgeErrorPolicies } from '@/lib/knowledge/api/route-policies'
+import { knowledgeOperations } from '@/lib/knowledge/application/operations'
+import {
+ listPersonalSourceSetupAccounts,
+ personalSourceSetup,
+} from '@/lib/knowledge/application/personal-source-setup'
+import {
+ SelectorConnectionUnavailableError,
+ SelectorContextUnavailableError,
+ SelectorOptionsUnavailableError,
+} from '@/lib/selectors/server/errors'
+import { IntegrationNotAllowedError } from '@/ee/access-control/utils/permission-check'
+
+const errorPolicy = extendInternalErrorPolicy(
+ internalKnowledgeErrorPolicies.connectAccount,
+ (error) => {
+ if (error instanceof SelectorConnectionUnavailableError)
+ return internalErrorResponse(error.status, {
+ error: 'Reconnect your account to choose projects or spaces',
+ })
+ if (error instanceof SelectorContextUnavailableError)
+ return internalErrorResponse(400, {
+ error: 'Enter your Atlassian site to choose projects or spaces',
+ })
+ if (error instanceof SelectorOptionsUnavailableError)
+ return internalErrorResponse(error.status, {
+ error:
+ 'Could not load projects or spaces. Check the site and account access, then try again.',
+ })
+ if (error instanceof IntegrationNotAllowedError)
+ return internalErrorResponse(403, { error: error.message })
+ return null
+ }
+)
+
+export const GET = defineInternalJsonRoute({
+ contract: listPersonalSourceSetupAccountsContract,
+ auth: internalSessionAuth,
+ operation: knowledgeOperations.listPersonalSourceSetupAccounts,
+ rateLimit: internalRateLimits.user({ bucketName: 'knowledge.search.personal-setup.accounts' }),
+ errorPolicy,
+ mapInput: ({ query }) => query,
+ useCase: listPersonalSourceSetupAccounts,
+ present: (data) => ({ success: true as const, data }),
+ staticResponseHeaders: { 'Cache-Control': 'private, no-store' },
+})
+
+export const POST = defineInternalJsonRoute({
+ contract: personalSourceSetupContract,
+ auth: internalSessionAuth,
+ operation: knowledgeOperations.personalSourceSetup,
+ rateLimit: internalRateLimits.user({ bucketName: 'knowledge.search.personal-setup' }),
+ errorPolicy,
+ parseOptions: { maxBodyBytes: 384 * 1024 },
+ mapInput: ({ body }) => body,
+ useCase: personalSourceSetup,
+ present: (data) => ({ success: true as const, data }),
+ staticResponseHeaders: { 'Cache-Control': 'private, no-store' },
+})
diff --git a/apps/sim/app/api/knowledge/sim-search/stats/route.ts b/apps/sim/app/api/knowledge/sim-search/stats/route.ts
new file mode 100644
index 00000000000..e7ae7eb9777
--- /dev/null
+++ b/apps/sim/app/api/knowledge/sim-search/stats/route.ts
@@ -0,0 +1,24 @@
+import { readOrganizationSearchStatsContract } from '@/lib/api/contracts/knowledge/search-stats'
+import {
+ defineInternalJsonRoute,
+ internalRateLimits,
+ internalSessionAuth,
+} from '@/lib/api/server/routes'
+import { internalKnowledgeErrorPolicies } from '@/lib/knowledge/api/route-policies'
+import { knowledgeOperations } from '@/lib/knowledge/application/operations'
+import { readOrganizationSearchStats } from '@/lib/knowledge/application/organization-search-stats'
+
+export const GET = defineInternalJsonRoute({
+ contract: readOrganizationSearchStatsContract,
+ auth: internalSessionAuth,
+ operation: knowledgeOperations.readOrganizationSearchStats,
+ rateLimit: internalRateLimits.user({
+ bucketName: 'organization-search-stats',
+ config: { maxTokens: 30, refillRate: 30, refillIntervalMs: 60_000 },
+ }),
+ errorPolicy: internalKnowledgeErrorPolicies.connectors,
+ mapInput: ({ query }) => query,
+ useCase: readOrganizationSearchStats,
+ present: (data) => ({ success: true as const, data }),
+ staticResponseHeaders: { 'Cache-Control': 'private, no-store' },
+})
diff --git a/apps/sim/app/o/[organizationId]/integrations/connect-account-options.tsx b/apps/sim/app/o/[organizationId]/integrations/connect-account-options.tsx
index c5e75a3a749..fe091b7cc4f 100644
--- a/apps/sim/app/o/[organizationId]/integrations/connect-account-options.tsx
+++ b/apps/sim/app/o/[organizationId]/integrations/connect-account-options.tsx
@@ -210,6 +210,7 @@ export function ConnectAccountOptions({
{enrollment.setupConnector && (
{
}
)
- it('keeps source setup fields after a failure without duplicating the toast inside the modal', async () => {
+ it('keeps manual source setup fields after a failure without duplicating the toast inside the modal', async () => {
const message = 'Connection unavailable'
mocks.enrollmentError = message
- mocks.setupConnector = SEARCH_CONNECTORS.find((connector) => connector.type === 'jira') ?? null
+ mocks.setupConnector =
+ SEARCH_CONNECTORS.find((connector) => connector.type === 'github') ?? null
await render()
expect(document.querySelector('[role="dialog"]')).not.toBeNull()
expect(document.body.textContent).not.toContain(message)
diff --git a/apps/sim/app/o/[organizationId]/settings/components/integrations/add-organization-source-modal.tsx b/apps/sim/app/o/[organizationId]/settings/components/integrations/add-organization-source-modal.tsx
new file mode 100644
index 00000000000..babcd4f2c91
--- /dev/null
+++ b/apps/sim/app/o/[organizationId]/settings/components/integrations/add-organization-source-modal.tsx
@@ -0,0 +1,106 @@
+'use client'
+
+import { type ReactNode, useState } from 'react'
+import {
+ ChipInput,
+ ChipModal,
+ ChipModalBody,
+ ChipModalField,
+ ChipModalFooter,
+ ChipModalHeader,
+} from '@sim/emcn'
+import { Search } from '@sim/emcn/icons'
+import { IntegrationTile } from '@/app/workspace/[workspaceId]/integrations/components/integrations-showcase'
+import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
+import {
+ RESOURCE_LIST_STACK,
+ SettingsResourceRow,
+} from '@/app/workspace/[workspaceId]/settings/components/settings-resource-row'
+import type { ConnectorMeta } from '@/connectors/types'
+
+interface AddOrganizationSourceModalProps {
+ sources: {
+ type: string
+ meta: ConnectorMeta
+ access: { admin: boolean; members: boolean }
+ }[]
+ pending: boolean
+ ready: boolean
+ feedback: ReactNode
+ onClose: () => void
+ onSelect: (type: string, accessMode: 'admin' | 'members') => void
+}
+
+export function AddOrganizationSourceModal({
+ sources,
+ pending,
+ ready,
+ feedback,
+ onClose,
+ onSelect,
+}: AddOrganizationSourceModalProps) {
+ const [search, setSearch] = useState('')
+ const query = search.trim().toLowerCase()
+ const visible = sources.filter(({ meta }) => meta.name.toLowerCase().includes(query))
+
+ return (
+ !open && onClose()}
+ srTitle='Add source'
+ >
+ Add source
+
+
+ setSearch(event.target.value)}
+ disabled={pending}
+ />
+
+
+ {feedback}
+
+ {visible.map(({ type, meta, access }) => {
+ const available = access.admin || access.members
+ return (
+ }
+ title={meta.name}
+ description={
+ !ready
+ ? 'Checking availability…'
+ : !available
+ ? 'Unavailable in this deployment'
+ : access.admin
+ ? meta.auth.mode === 'oauth' &&
+ meta.auth.adminCredentialType === 'service_account'
+ ? 'Connect a service account'
+ : 'Connect an admin account'
+ : type === 'slack'
+ ? 'Set up your Slack app'
+ : 'Connect member accounts'
+ }
+ disabled={pending || !ready || !available}
+ onClick={() => onSelect(type, access.admin ? 'admin' : 'members')}
+ clickLabel={`Set up ${meta.name}`}
+ navigable={ready && available}
+ />
+ )
+ })}
+ {visible.length === 0 && (
+ No matching sources
+ )}
+
+
+
+
+
+ )
+}
diff --git a/apps/sim/app/o/[organizationId]/settings/components/integrations/organization-integrations-settings.test.tsx b/apps/sim/app/o/[organizationId]/settings/components/integrations/organization-integrations-settings.test.tsx
index 952880c34ee..45b6307162e 100644
--- a/apps/sim/app/o/[organizationId]/settings/components/integrations/organization-integrations-settings.test.tsx
+++ b/apps/sim/app/o/[organizationId]/settings/components/integrations/organization-integrations-settings.test.tsx
@@ -23,6 +23,14 @@ vi.mock(
'@/app/o/[organizationId]/settings/components/integrations/organization-integrations-setup',
() => ({ OrganizationIntegrationsSetup: () => Provider setup
})
)
+vi.mock(
+ '@/app/o/[organizationId]/settings/components/integrations/organization-source-stats',
+ () => ({
+ OrganizationSourceStats: ({ organizationId }: { organizationId: string }) => (
+ Stats for {organizationId}
+ ),
+ })
+)
vi.mock('@/hooks/queries/organization-accounts', () => ({
useOrganizationAccounts: mocks.accounts,
useUpdateOrganizationAccounts: () => ({
@@ -53,6 +61,7 @@ describe('organization integration invitations', () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true)
mocks.context.mockReturnValue({ organization: { id: 'org-a' }, viewer: { isAdmin: true } })
mocks.accounts.mockReturnValue({
+ isSuccess: true,
data: { credentialGroup: { id: 'group-a', options: [] } },
error: null,
refetch: mocks.refetch,
@@ -111,9 +120,9 @@ describe('organization integration invitations', () => {
)
)
const item = document.querySelector('[role="menuitem"]')
- expect(item?.textContent).toBe('Refresh connection settings')
+ expect(item?.textContent).toBe('Update sign-in settings')
await act(async () => item?.click())
- expect(document.body.textContent).toContain('Affected accounts will need to reconnect.')
+ expect(document.body.textContent).toContain('People whose settings changed must reconnect.')
}
it('keeps provider setup as the default and sends manual invitations from People to this org', async () => {
@@ -147,6 +156,7 @@ describe('organization integration invitations', () => {
it('refreshes saved provider identities only after choosing the maintenance action and confirming', async () => {
mocks.accounts.mockReturnValue({
+ isSuccess: true,
data: {
credentialGroup: {
id: 'group-a',
@@ -173,7 +183,7 @@ describe('organization integration invitations', () => {
mocks.update.mockImplementationOnce((_input, { onSuccess }) => onSuccess())
await render()
await openRefresh()
- await click('Refresh')
+ await click('Update')
expect(mocks.update).toHaveBeenCalledWith(
{
organizationId: 'org-a',
@@ -198,26 +208,27 @@ describe('organization integration invitations', () => {
},
expect.any(Object)
)
- expect(toast.success).toHaveBeenCalledWith('Connection settings refreshed')
+ expect(toast.success).toHaveBeenCalledWith('Sign-in settings updated')
expect(document.querySelector('[role="dialog"]')).toBeNull()
})
it('keeps failed refreshes open for retry and blocks duplicate submissions', async () => {
mocks.accounts.mockReturnValue({
+ isSuccess: true,
data: { credentialGroup: { id: 'group-a', options: [{ provider: 'gmail' }] } },
error: null,
})
await render()
await openRefresh()
- await click('Refresh')
+ await click('Update')
mocks.updateError = new Error('Update denied')
await render()
expect(document.body.textContent).toContain('Update denied')
expect(document.querySelector('[role="dialog"]')).not.toBeNull()
mocks.updatePending = true
await render()
- expect(findButton('Refresh')).toBeDisabled()
+ expect(findButton('Update')).toBeDisabled()
expect(mocks.update).toHaveBeenCalledOnce()
})
@@ -233,11 +244,16 @@ describe('organization integration invitations', () => {
expect(mocks.people).toHaveBeenLastCalledWith('org-a', '', { enabled: true })
})
- it('loads people alongside setup but waits for the pool before allowing invitations', async () => {
- mocks.accounts.mockReturnValue({ data: undefined, error: null, isPending: true })
- await render('?tab=people')
+ it('waits for integration options before loading filtered people or allowing invitations', async () => {
+ mocks.accounts.mockReturnValue({
+ isSuccess: false,
+ data: undefined,
+ error: null,
+ isPending: true,
+ })
+ await render('?tab=people&integration=jira')
expect(mocks.accounts).toHaveBeenLastCalledWith('org-a')
- expect(mocks.people).toHaveBeenLastCalledWith('org-a', '', { enabled: true })
+ expect(mocks.people).toHaveBeenLastCalledWith('org-a', '', { enabled: false })
expect(container.textContent).toContain('Loading connected accounts')
expect(container.textContent).not.toContain('No people invited yet')
expect(findButton('Request connections')).toBeDisabled()
@@ -245,10 +261,11 @@ describe('organization integration invitations', () => {
expect(document.querySelector('[role="dialog"]')).toBeNull()
mocks.accounts.mockReturnValue({
+ isSuccess: true,
data: { credentialGroup: { id: 'group-a', options: [] } },
error: null,
})
- await render('?tab=people')
+ await render('?tab=people&integration=jira')
expect(container.textContent).not.toContain('Loading connected accounts')
expect(findButton('Request connections')).not.toBeDisabled()
await click('Request connections')
@@ -257,14 +274,23 @@ describe('organization integration invitations', () => {
})
it('stops the people query when setup resolves without a pool and preserves the setup action', async () => {
- mocks.accounts.mockReturnValue({ data: undefined, error: null, isPending: true })
+ mocks.accounts.mockReturnValue({
+ isSuccess: false,
+ data: undefined,
+ error: null,
+ isPending: true,
+ })
mocks.people.mockReturnValue({ error: new Error('Organization accounts not configured') })
- await render('?tab=people')
- expect(mocks.people).toHaveBeenLastCalledWith('org-a', '', { enabled: true })
+ await render('?tab=people&integration=jira')
+ expect(mocks.people).toHaveBeenLastCalledWith('org-a', '', { enabled: false })
expect(container.textContent).not.toContain('Organization accounts not configured')
- mocks.accounts.mockReturnValue({ data: { credentialGroup: null }, error: null })
- await render('?tab=people')
+ mocks.accounts.mockReturnValue({
+ isSuccess: true,
+ data: { credentialGroup: null },
+ error: null,
+ })
+ await render('?tab=people&integration=jira')
expect(mocks.people).toHaveBeenLastCalledWith('org-a', '', { enabled: false })
expect(container.textContent).toContain('before requesting connections')
expect(container.textContent).not.toContain('Organization accounts not configured')
@@ -272,7 +298,11 @@ describe('organization integration invitations', () => {
})
it('sends an org without a credential group back to provider setup before invitations', async () => {
- mocks.accounts.mockReturnValue({ data: { credentialGroup: null }, error: null })
+ mocks.accounts.mockReturnValue({
+ isSuccess: true,
+ data: { credentialGroup: null },
+ error: null,
+ })
await render('?tab=people')
expect(container.textContent).toContain('before requesting connections')
expect(mocks.people).toHaveBeenLastCalledWith('org-a', '', { enabled: false })
@@ -284,6 +314,7 @@ describe('organization integration invitations', () => {
it('surfaces account lookup errors instead of treating them as missing setup', async () => {
mocks.accounts.mockReturnValue({
+ isSuccess: true,
error: new Error('Account access denied'),
refetch: mocks.refetch,
})
@@ -304,4 +335,105 @@ describe('organization integration invitations', () => {
expect(mocks.people).not.toHaveBeenCalled()
expect(mocks.invite).not.toHaveBeenCalled()
})
+ it('opens organization stats without loading people', async () => {
+ await render()
+ await click('Stats')
+ expect(container.textContent).toContain('Stats for org-a')
+ expect(mocks.people).not.toHaveBeenCalled()
+ })
+
+ it('filters connection summaries and requests to the selected integration, then returns to All', async () => {
+ mocks.accounts.mockReturnValue({
+ isSuccess: true,
+ data: {
+ credentialGroup: {
+ id: 'group-a',
+ options: [
+ { id: 'jira-option', provider: 'jira', status: 'active' },
+ { id: 'gmail-option', provider: 'gmail', status: 'active' },
+ { id: 'old-option', provider: 'confluence', status: 'revoked' },
+ ],
+ },
+ },
+ })
+ await render('?tab=people&integration=jira&credential-group-people=alex')
+ expect(mocks.people).toHaveBeenLastCalledWith('org-a', 'alex', {
+ enabled: true,
+ optionId: 'jira-option',
+ })
+ expect(findButton('Filter people by integration').textContent).toContain('Jira')
+ await click('Request connections')
+ expect(document.querySelector('[role="dialog"]')?.textContent).toContain(
+ 'Request Jira connections'
+ )
+ await click('Cancel')
+ await act(async () =>
+ findButton('Filter people by integration').dispatchEvent(
+ new MouseEvent('pointerdown', { bubbles: true, button: 0 })
+ )
+ )
+ const all = Array.from(document.querySelectorAll('[role="menuitem"]')).find(
+ (item) => item.textContent === 'All integrations'
+ )
+ expect(all).toBeDefined()
+ expect(document.querySelector('[role="menu"]')?.textContent).not.toContain('Confluence')
+ await act(async () => all?.click())
+ await vi.waitFor(() =>
+ expect(mocks.people).toHaveBeenLastCalledWith('org-a', 'alex', { enabled: true })
+ )
+ expect(container.querySelector('input[placeholder="Search people..."]')).toHaveValue('alex')
+ })
+
+ it.each(['', '&integration=gmail'])(
+ 'defaults to All on navigation with one integration and initial filter %s',
+ async (filter) => {
+ mocks.accounts.mockReturnValue({
+ isSuccess: true,
+ data: {
+ credentialGroup: {
+ id: 'group-a',
+ options: [{ id: 'gmail-option', provider: 'gmail', status: 'active' }],
+ },
+ },
+ })
+ await render(`?tab=people${filter}`)
+ expect(findButton('Filter people by integration').textContent).toContain(
+ filter ? 'Gmail' : 'All integrations'
+ )
+ expect(mocks.people).toHaveBeenLastCalledWith('org-a', '', {
+ enabled: true,
+ ...(filter ? { optionId: 'gmail-option' } : {}),
+ })
+ await click('Sources')
+ await click('People')
+ expect(findButton('Filter people by integration').textContent).toContain('All integrations')
+ expect(mocks.people).toHaveBeenLastCalledWith('org-a', '', { enabled: true })
+ }
+ )
+
+ it('preserves Slack setup recovery in People without hiding existing connections', async () => {
+ mocks.accounts.mockReturnValue({
+ isSuccess: true,
+ data: {
+ credentialGroup: {
+ id: 'group-a',
+ options: [
+ {
+ id: 'slack-option',
+ provider: 'slack',
+ status: 'active',
+ configurationStatus: 'needs_update',
+ },
+ ],
+ },
+ },
+ })
+ await render('?tab=people&integration=slack')
+ expect(mocks.people).toHaveBeenLastCalledWith('org-a', '', {
+ enabled: true,
+ optionId: 'slack-option',
+ })
+ expect(findButton('Request connections')).toBeDisabled()
+ expect(container.textContent).toContain('Update the Slack app from Sources')
+ })
})
diff --git a/apps/sim/app/o/[organizationId]/settings/components/integrations/organization-integrations-settings.tsx b/apps/sim/app/o/[organizationId]/settings/components/integrations/organization-integrations-settings.tsx
index 5a88385585a..4753f09eb96 100644
--- a/apps/sim/app/o/[organizationId]/settings/components/integrations/organization-integrations-settings.tsx
+++ b/apps/sim/app/o/[organizationId]/settings/components/integrations/organization-integrations-settings.tsx
@@ -2,17 +2,21 @@
import { useState } from 'react'
import { Chip, ChipConfirmModal, ChipModalError, ChipSwitch, toast } from '@sim/emcn'
-import { useQueryState } from 'nuqs'
+import { useQueryStates } from 'nuqs'
import { getOrganizationAccountUpdateOptions } from '@/lib/credential-groups/organization-account-options'
import { useOrganizationContext } from '@/app/o/[organizationId]/providers/organization-provider'
import { OrganizationIntegrationsSetup } from '@/app/o/[organizationId]/settings/components/integrations/organization-integrations-setup'
-import { organizationIntegrationsTabParam } from '@/app/o/[organizationId]/settings/components/integrations/search-params'
+import { OrganizationSourcePeople } from '@/app/o/[organizationId]/settings/components/integrations/organization-source-people'
+import { OrganizationSourceStats } from '@/app/o/[organizationId]/settings/components/integrations/organization-source-stats'
+import {
+ organizationIntegrationsTabParam,
+ organizationPeopleIntegrationParam,
+} from '@/app/o/[organizationId]/settings/components/integrations/search-params'
import { RowActionsMenu } from '@/app/workspace/[workspaceId]/settings/components/row-actions-menu'
import {
SettingsEmptyState,
SettingsQueryErrorState,
} from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
-import { OrganizationAccountPeople } from '@/ee/credential-groups/components/organization-account-people'
import {
useOrganizationAccounts,
useUpdateOrganizationAccounts,
@@ -20,10 +24,10 @@ import {
export function OrganizationIntegrationsSettings() {
const { organization, viewer } = useOrganizationContext()
- const [tab, setTab] = useQueryState(
- organizationIntegrationsTabParam.key,
- organizationIntegrationsTabParam.parser
- )
+ const [{ tab }, setNavigation] = useQueryStates({
+ [organizationIntegrationsTabParam.key]: organizationIntegrationsTabParam.parser,
+ [organizationPeopleIntegrationParam.key]: organizationPeopleIntegrationParam.parser,
+ })
const accounts = useOrganizationAccounts(viewer.isAdmin ? organization.id : undefined)
const update = useUpdateOrganizationAccounts()
const [refreshOpen, setRefreshOpen] = useState(false)
@@ -39,58 +43,68 @@ export function OrganizationIntegrationsSettings() {
{
onSuccess: () => {
setRefreshOpen(false)
- toast.success('Connection settings refreshed')
+ toast.success('Sign-in settings updated')
},
}
)
}
if (!viewer.isAdmin) return null
+ const tabs = (
+ void setNavigation({ tab: value, integration: null })}
+ options={[
+ { value: 'providers', label: 'Sources' },
+ { value: 'people', label: 'People' },
+ { value: 'stats', label: 'Stats' },
+ ]}
+ />
+ )
+
return (
-
-
void setTab(value)}
- options={[
- { value: 'providers', label: 'Sources' },
- { value: 'people', label: 'People' },
- ]}
- />
- {tab === 'providers' && !accounts.error && group && group.options.length > 0 && (
- {
- update.reset()
- setRefreshOpen(true)
+ {tab === 'providers' && (
+
+ {tabs}
+ {tab === 'providers' && !accounts.error && group && group.options.length > 0 && (
+ {
+ update.reset()
+ setRefreshOpen(true)
+ },
},
- },
- ]}
- />
- )}
-
+ ]}
+ />
+ )}
+
+ )}
{
if (!update.isPending) setRefreshOpen(open)
}}
- title='Refresh connection settings?'
- text='Apply the latest sign-in settings to all integrations. Affected accounts will need to reconnect.'
- confirm={{ label: 'Refresh', pending: update.isPending, onClick: refreshConnections }}
+ title='Update sign-in settings?'
+ text='Apply Sim’s current OAuth app and permission settings to member connections. People whose settings changed must reconnect. This does not sync content.'
+ confirm={{ label: 'Update', pending: update.isPending, onClick: refreshConnections }}
>
{update.error?.message}
{tab === 'providers' &&
}
+ {tab === 'stats' &&
}
{tab === 'people' && (
-
Add a source that uses member accounts before requesting connections.
- void setTab('providers')}>View sources
+ void setNavigation({ tab: 'providers', integration: null })}>
+ View sources
+
) : undefined
}
diff --git a/apps/sim/app/o/[organizationId]/settings/components/integrations/organization-integrations-setup.test.tsx b/apps/sim/app/o/[organizationId]/settings/components/integrations/organization-integrations-setup.test.tsx
index 9c1f75f361e..bb3eab8e511 100644
--- a/apps/sim/app/o/[organizationId]/settings/components/integrations/organization-integrations-setup.test.tsx
+++ b/apps/sim/app/o/[organizationId]/settings/components/integrations/organization-integrations-setup.test.tsx
@@ -8,6 +8,9 @@ import type { OrganizationSearchProviderSummary } from '@/lib/api/contracts/know
const mocks = vi.hoisted(() => ({
admin: true,
+ memberAccess: true,
+ mirroredAccess: true,
+ updateUrl: vi.fn(),
overview: vi.fn(),
push: vi.fn(),
add: vi.fn(),
@@ -25,7 +28,7 @@ vi.mock('@/app/o/[organizationId]/providers/organization-provider', () => ({
useOrganizationContext: () => ({
organization: { id: 'org-one' },
viewer: { isAdmin: mocks.admin },
- searchAccess: { memberScoped: true, sourceMirrored: true },
+ searchAccess: { memberScoped: mocks.memberAccess, sourceMirrored: mocks.mirroredAccess },
}),
}))
vi.mock('@/hooks/queries/kb/connectors', () => ({ useOrganizationSearchOverview: mocks.overview }))
@@ -73,6 +76,8 @@ let container: HTMLDivElement
beforeEach(() => {
vi.clearAllMocks()
mocks.admin = true
+ mocks.memberAccess = true
+ mocks.mirroredAccess = true
mocks.pending = false
mocks.mutationError = null
mocks.add.mockReset()
@@ -83,6 +88,7 @@ beforeEach(() => {
['confluence', true],
['google-drive', true],
['google-email', true],
+ ['jira', true],
]),
isIntegrationAvailabilityReady: true,
refetchIntegrationAvailability: mocks.refetch,
@@ -110,7 +116,7 @@ afterEach(async () => {
async function render(searchParams = '') {
await act(async () =>
root.render(
-
+
@@ -128,253 +134,280 @@ async function click(label: string) {
await act(async () => button!.click())
}
-describe('organization integration management entry', () => {
- it('uses Sources terminology in search and its empty state', async () => {
- await render('?search=not-a-real-source')
- expect(container.querySelector('input[placeholder="Search sources..."]')).toHaveValue(
- 'not-a-real-source'
- )
- expect(container.textContent).toContain('No matching sources')
- expect(container.textContent).not.toContain('No matching integrations')
- })
- it('offers Drive account management before anyone has connected', async () => {
- mocks.overview.mockReturnValue({
- data: {
- providers: [
- {
- connectorType: 'google_drive',
- approved: true,
- sourceCount: 0,
- status: 'waiting_for_connections',
- issue: null,
- isSyncing: false,
- },
- ],
- },
- isPending: false,
+async function expectSetup(type: string | null, access: string | null = null) {
+ await act(async () => {
+ await vi.waitFor(() => {
+ const params = mocks.updateUrl.mock.calls.at(-1)?.[0].searchParams
+ expect(params).toBeDefined()
+ expect(params.get('addConnector')).toBe(type)
+ expect(params.get('source-access')).toBe(access)
})
- await render()
- expect(document.querySelector('a[aria-label="Manage Google Drive"]')).toHaveAttribute(
- 'href',
- '/o/org-one/settings/integrations/providers/google_drive'
- )
- expect(document.querySelector('a[aria-label="Set up Google Drive"]')).toBeNull()
- expect(container.textContent).toContain('Waiting for connections')
})
- it('shows the stable catalog with switches and separate setup and management links', async () => {
+}
+
+async function fillPicker(value: string) {
+ const input = document.querySelector('[aria-label="Find a source"]')
+ expect(input).not.toBeNull()
+ await act(async () => {
+ Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set?.call(input, value)
+ input?.dispatchEvent(new Event('input', { bubbles: true }))
+ })
+}
+
+describe('organization integration management entry', () => {
+ it('uses navigable settings rows for added sources without switches or setup buttons', async () => {
await render()
- expect(document.querySelector('a[aria-label="Manage Gmail"]')).toHaveAttribute(
+ expect(container.textContent).toContain('Waiting for connections')
+ expect(container.textContent).toContain('Deactivated · 2 connections')
+ expect(container.querySelector('a[aria-label="Manage Gmail"]')).toHaveAttribute(
'href',
'/o/org-one/settings/integrations/providers/gmail'
)
- expect(document.querySelector('a[aria-label="Manage Google Drive"]')).toHaveAttribute(
- 'href',
- '/o/org-one/settings/integrations/providers/google_drive'
- )
- expect(container.textContent).toContain('Waiting for connections')
- expect(container.textContent).not.toContain('Needs setup')
- expect(container.textContent).toContain('Disabled')
- expect(container.textContent).toContain('Confluence')
- expect(container.textContent).not.toContain('Add integration')
- expect(document.querySelector('[aria-label="Allow Gmail in Sim Search"]')).toHaveAttribute(
- 'aria-checked',
- 'true'
- )
- expect(
- document.querySelector('[aria-label="Allow Google Drive in Sim Search"]')
- ).toHaveAttribute('aria-checked', 'false')
- expect(document.querySelector('a[aria-label="Set up Confluence"]')).toBeNull()
- })
-
- it('offers the catalog for a new organization without a modal or writes', async () => {
- mocks.overview.mockReturnValue({ data: { providers: [] }, isPending: false })
- await render()
- expect(container.textContent).toContain('Confluence')
- expect(container.textContent).toContain('Google Drive')
- expect(document.querySelector('[role="dialog"]')).toBeNull()
+ expect(container.querySelector('a[aria-label="Manage Google Drive"]')).not.toBeNull()
+ expect(container.textContent).not.toContain('Confluence')
+ expect(container.querySelector('[role="switch"]')).toBeNull()
+ expect(container.querySelector('a[aria-label="Set up Gmail"]')).toBeNull()
expect(mocks.add).not.toHaveBeenCalled()
})
- it('enables in place and shows setup after the server confirms approval', async () => {
- await render()
- const row = document.querySelector('[aria-label="Allow Confluence in Sim Search"]')
- await click('Allow Confluence in Sim Search')
- expect(mocks.add).toHaveBeenCalledExactlyOnceWith(
- { organizationId: 'org-one', connectorType: 'confluence', approved: true },
- expect.any(Object)
- )
- expect(row).toHaveAttribute('aria-checked', 'false')
+ it('keeps approved sources with unfinished setup visible', async () => {
mocks.overview.mockReturnValue({
data: {
providers: [
- ...providers,
{
+ ...providers[0],
connectorType: 'confluence',
- approved: true,
- sourceCount: 0,
status: 'needs_setup',
- isSyncing: false,
},
],
},
isPending: false,
})
await render()
- expect(document.querySelector('[aria-label="Allow Confluence in Sim Search"]')).toBe(row)
- expect(row).toHaveAttribute('aria-checked', 'true')
- expect(document.querySelector('a[aria-label="Set up Confluence"]')).not.toBeNull()
- expect(mocks.push).not.toHaveBeenCalled()
- expect(document.querySelector('[role="dialog"]')).toBeNull()
+ expect(container.textContent).toContain('Setup required')
+ expect(container.querySelector('a[aria-label="Manage Confluence"]')).not.toBeNull()
})
- it('requires confirmation before disabling existing sources and lets the admin cancel', async () => {
- mocks.overview.mockReturnValue({
- data: { providers: [{ ...providers[1], approved: true }] },
- isPending: false,
- })
+ it('opens the catalog from the header without changing any approvals', async () => {
+ mocks.overview.mockReturnValue({ data: { providers: [] }, isPending: false })
await render()
- await click('Allow Google Drive in Sim Search')
- expect(document.querySelector('[role="dialog"]')).toHaveTextContent(
- 'Sources and connected accounts are preserved.'
- )
+ expect(container.textContent).toContain('No sources yet. Add a source to get started.')
+ expect(document.querySelector('[role="dialog"]')).toBeNull()
+ await click('Add source')
+ expect(document.querySelector('[role="dialog"]')).toHaveTextContent('Confluence')
+ expect(document.querySelector('[role="dialog"]')).toHaveTextContent('Jira')
+ expect(document.querySelector('[role="dialog"]')).toHaveTextContent('Connect a service account')
+ expect(document.querySelector('[role="dialog"]')).toHaveTextContent('Connect member accounts')
expect(mocks.add).not.toHaveBeenCalled()
await click('Cancel')
expect(document.querySelector('[role="dialog"]')).toBeNull()
+ await expectSetup(null)
expect(mocks.add).not.toHaveBeenCalled()
- await click('Allow Google Drive in Sim Search')
- mocks.add.mockImplementation((_input, options) => options.onSuccess())
- await click('Deactivate')
+ })
+
+ it('approves a new source before opening its central configuration form', async () => {
+ await render('?addConnector=')
+ await click('Set up Confluence')
expect(mocks.add).toHaveBeenCalledExactlyOnceWith(
- { organizationId: 'org-one', connectorType: 'google_drive', approved: false },
+ { organizationId: 'org-one', connectorType: 'confluence', approved: true },
expect.any(Object)
)
+ expect(document.querySelector('[role="dialog"]')).not.toBeNull()
+ await act(async () => mocks.add.mock.calls[0][1].onSuccess())
+ await expectSetup('confluence')
expect(document.querySelector('[role="dialog"]')).toBeNull()
})
- it('allows disabling an integration with no sources in place', async () => {
- await render()
- await click('Allow Gmail in Sim Search')
- expect(mocks.add).toHaveBeenCalledExactlyOnceWith(
- { organizationId: 'org-one', connectorType: 'gmail', approved: false },
+ it('opens an approved source directly without a duplicate approval', async () => {
+ await render('?addConnector=&search=retained')
+ await click('Set up Gmail')
+ await expectSetup('gmail')
+ expect(mocks.updateUrl.mock.calls.at(-1)?.[0].searchParams.get('search')).toBe('retained')
+ expect(mocks.add).not.toHaveBeenCalled()
+ })
+
+ it.each(['jira', 'confluence'])(
+ 'uses member setup for %s when central indexing is unavailable',
+ async (type) => {
+ mocks.mirroredAccess = false
+ mocks.add.mockImplementation((_input, options) => options.onSuccess())
+ await render('?addConnector=')
+ await click(`Set up ${type === 'jira' ? 'Jira' : 'Confluence'}`)
+ await expectSetup(type, 'members')
+ }
+ )
+
+ it('routes Slack into the existing member setup with its custom-app step', async () => {
+ mocks.availability.mockReturnValue({
+ integrationAvailability: new Map([['slack_v2', { state: 'limited' }]]),
+ oauthServiceAvailability: new Map(),
+ isIntegrationAvailabilityReady: true,
+ })
+ mocks.add.mockImplementation((_input, options) => options.onSuccess())
+ await render('?addConnector=')
+ expect(document.querySelector('[role="dialog"]')).toHaveTextContent('Set up your Slack app')
+ await click('Set up Slack')
+ await expectSetup('slack', 'members')
+ })
+
+ it('reactivates a retained source before opening another configuration', async () => {
+ mocks.add.mockImplementation((_input, options) => options.onSuccess())
+ await render('?addConnector=')
+ await click('Set up Google Drive')
+ await expectSetup('google_drive')
+ expect(mocks.add).toHaveBeenCalledWith(
+ { organizationId: 'org-one', connectorType: 'google_drive', approved: true },
expect.any(Object)
)
- expect(document.querySelector('[role="dialog"]')).toBeNull()
})
- it('keeps the confirmed state and reports failed approval changes', async () => {
+ it('keeps the picker open and toasts a failed approval for retry', async () => {
mocks.add.mockImplementation((_input, options) =>
- options.onError(new Error('Could not update integration'))
+ options.onError(new Error('Could not add source'))
)
- await render()
- await click('Allow Confluence in Sim Search')
- expect(toast.error).toHaveBeenCalledWith('Could not update integration')
- expect(document.querySelector('[aria-label="Allow Confluence in Sim Search"]')).toHaveAttribute(
- 'aria-checked',
- 'false'
- )
- expect(document.querySelector('a[aria-label="Set up Confluence"]')).toBeNull()
+ await render('?addConnector=')
+ await click('Set up Confluence')
+ expect(toast.error).toHaveBeenCalledWith('Could not add source')
+ expect(document.querySelector('[role="dialog"]')).not.toBeNull()
+ expect(mocks.updateUrl).not.toHaveBeenCalled()
})
- it('keeps a failed deactivation in the confirmation dialog for retry', async () => {
- mocks.overview.mockReturnValue({
- data: { providers: [{ ...providers[1], approved: true }] },
- isPending: false,
+ it('prevents another selection or dismissal while approval is pending', async () => {
+ mocks.pending = true
+ await render('?addConnector=')
+ expect(document.querySelector('button[aria-label="Set up Confluence"]')).toBeNull()
+ expect(document.querySelector('[aria-label="Find a source"]')).toBeDisabled()
+ await click('Cancel')
+ expect(document.querySelector('[role="dialog"]')).not.toBeNull()
+ expect(mocks.add).not.toHaveBeenCalled()
+ expect(mocks.updateUrl).not.toHaveBeenCalled()
+ })
+
+ it('explains unavailable providers in the picker without allowing setup', async () => {
+ mocks.availability.mockReturnValue({
+ integrationAvailability: new Map(),
+ oauthServiceAvailability: new Map(),
+ isIntegrationAvailabilityReady: true,
})
- await render()
- await click('Allow Google Drive in Sim Search')
- await click('Deactivate')
- mocks.mutationError = new Error('Could not deactivate integration')
- await render()
+ await render('?addConnector=')
expect(document.querySelector('[role="dialog"]')).toHaveTextContent(
- 'Could not deactivate integration'
+ 'Unavailable in this deployment'
)
- expect(
- document.querySelector('[aria-label="Allow Google Drive in Sim Search"]')
- ).toHaveAttribute('aria-checked', 'true')
- })
-
- it('prevents duplicate changes while a mutation is pending', async () => {
- mocks.pending = true
- await render()
- await click('Allow Confluence in Sim Search')
+ expect(document.querySelector('button[aria-label="Set up Confluence"]')).toBeNull()
expect(mocks.add).not.toHaveBeenCalled()
- expect(document.querySelector('[aria-label="Allow Confluence in Sim Search"]')).toBeDisabled()
})
- it('blocks unavailable setup while keeping existing sources manageable and deactivatable', async () => {
+ it('keeps existing unavailable sources manageable', async () => {
mocks.availability.mockReturnValue({
integrationAvailability: new Map(),
oauthServiceAvailability: new Map(),
isIntegrationAvailabilityReady: true,
})
+ await render()
+ expect(container.querySelector('a[aria-label="Manage Gmail"]')).not.toBeNull()
+ expect(container.querySelector('a[aria-label="Manage Google Drive"]')).not.toBeNull()
+ })
+
+ it('filters the picker independently from the main source list and resets when reopened', async () => {
+ await render('?search=gmail')
+ await click('Add source')
+ await fillPicker('confluence')
+ const dialog = document.querySelector('[role="dialog"]')
+ expect(dialog).toHaveTextContent('Confluence')
+ expect(dialog).not.toHaveTextContent('Google Drive')
+ expect(container.querySelector('a[aria-label="Manage Gmail"]')).not.toBeNull()
+ await fillPicker('no-such-source')
+ expect(dialog).toHaveTextContent('No matching sources')
+ await click('Cancel')
+ await click('Add source')
+ expect(document.querySelector('[aria-label="Find a source"]')).toHaveValue('')
+ expect(document.querySelector('[role="dialog"]')).toHaveTextContent('Google Drive')
+ expect(mocks.add).not.toHaveBeenCalled()
+ })
+
+ it('keeps source health visible alongside the configuration count', async () => {
mocks.overview.mockReturnValue({
- data: { providers: [{ ...providers[1], approved: true }] },
+ data: {
+ providers: [
+ {
+ ...providers[1],
+ approved: true,
+ status: 'needs_attention',
+ issue: 'sync_failed',
+ },
+ ],
+ },
isPending: false,
})
await render()
- expect(document.querySelector('[aria-label="Allow Confluence in Sim Search"]')).toBeDisabled()
- expect(
- document.querySelector('[aria-label="Allow Google Drive in Sim Search"]')
- ).not.toBeDisabled()
- expect(document.querySelector('a[aria-label="Manage Google Drive"]')).not.toBeNull()
- expect(container.textContent).toContain('Unavailable in this deployment')
- await click('Allow Confluence in Sim Search')
- expect(mocks.add).not.toHaveBeenCalled()
+ expect(container.textContent).toContain('Sync failed · 2 connections')
})
- it('fails closed while availability loads and offers retry if it fails', async () => {
+ it('fails closed and offers retry when availability fails inside the picker', async () => {
mocks.availability.mockReturnValue({
integrationAvailability: new Map(),
oauthServiceAvailability: new Map(),
- isIntegrationAvailabilityReady: false,
+ isIntegrationAvailabilityReady: true,
integrationAvailabilityError: new Error('Could not load connection availability'),
refetchIntegrationAvailability: mocks.refetch,
})
- await render()
- expect(document.querySelector('[aria-label="Allow Confluence in Sim Search"]')).toBeDisabled()
+ await render('?addConnector=')
+ expect(document.querySelector('[role="dialog"]')).toHaveTextContent(
+ 'Could not load connection availability'
+ )
+ expect(document.querySelector('button[aria-label="Set up Confluence"]')).toBeNull()
await click('Try again')
expect(mocks.refetch).toHaveBeenCalledOnce()
})
- it('does not hide actionable sync failures behind a source count', async () => {
- mocks.overview.mockReturnValue({
- data: { providers: [{ ...providers[1], approved: true, status: 'needs_attention' }] },
- isPending: false,
- })
- await render()
- expect(container.textContent).toContain('Sync failed')
- expect(document.querySelector('a[aria-label="Manage Google Drive"]')).not.toBeNull()
- })
-
- it('does not turn a failed overview into unapproved switches', async () => {
+ it('does not turn a failed overview into an empty source list', async () => {
mocks.overview.mockReturnValue({
error: new Error('Could not load sources'),
isError: true,
refetch: mocks.refetch,
})
await render()
- expect(document.querySelector('[role="switch"]')).toBeNull()
+ expect(container.textContent).not.toContain('No sources yet')
+ expect(
+ Array.from(container.querySelectorAll('button')).find(
+ (node) => node.textContent === 'Add source'
+ )
+ ).toBeDisabled()
await click('Try again')
expect(mocks.refetch).toHaveBeenCalledOnce()
})
- it('keeps loading distinct from unapproved integrations', async () => {
+ it('keeps loading distinct from a new organization', async () => {
mocks.overview.mockReturnValue({ isPending: true })
await render()
expect(container.textContent).toContain('Loading sources')
- expect(document.querySelector('[role="switch"]')).toBeNull()
+ expect(container.textContent).not.toContain('No sources yet')
+ expect(
+ Array.from(container.querySelectorAll('button')).find(
+ (node) => node.textContent === 'Add source'
+ )
+ ).toBeDisabled()
})
it('does not expose admin controls or load admin data for members', async () => {
mocks.admin = false
- await render()
+ await render('?addConnector=')
expect(mocks.overview).toHaveBeenLastCalledWith('org-one', { enabled: false })
expect(container.textContent).toBe('')
+ expect(document.querySelector('[role="dialog"]')).toBeNull()
+ })
+
+ it('does not expose setup when Search is disabled', async () => {
+ mocks.memberAccess = false
+ mocks.mirroredAccess = false
+ await render('?addConnector=')
+ expect(container.textContent).toContain('Search sources are not enabled')
+ expect(document.querySelector('[role="dialog"]')).toBeNull()
})
- it('filters the whole catalog without changing approvals', async () => {
- await render('?search=confluence')
- expect(container.textContent).toContain('Confluence')
+ it('filters added sources without changing approvals', async () => {
+ await render('?search=gmail')
+ expect(container.textContent).toContain('Gmail')
expect(container.textContent).not.toContain('Google Drive')
expect(mocks.add).not.toHaveBeenCalled()
})
diff --git a/apps/sim/app/o/[organizationId]/settings/components/integrations/organization-integrations-setup.tsx b/apps/sim/app/o/[organizationId]/settings/components/integrations/organization-integrations-setup.tsx
index a3de964ef15..359388941bd 100644
--- a/apps/sim/app/o/[organizationId]/settings/components/integrations/organization-integrations-setup.tsx
+++ b/apps/sim/app/o/[organizationId]/settings/components/integrations/organization-integrations-setup.tsx
@@ -1,19 +1,21 @@
'use client'
-import { useState } from 'react'
-import { ChipConfirmModal, ChipLink, ChipModalError, Switch, toast } from '@sim/emcn'
+import { toast } from '@sim/emcn'
+import { Plus } from '@sim/emcn/icons'
+import { useQueryStates } from 'nuqs'
import { SettingsPanel } from '@/components/settings/settings-panel'
import { organizationRoutes } from '@/lib/navigation/paths'
-import {
- canConnectWithDefaults,
- getConnectorAccessAvailability,
- SEARCH_SOURCE_TYPES,
-} from '@/lib/sim-search/connectors'
+import { getConnectorAccessAvailability, SEARCH_SOURCE_TYPES } from '@/lib/sim-search/connectors'
import { useOrganizationContext } from '@/app/o/[organizationId]/providers/organization-provider'
+import { AddOrganizationSourceModal } from '@/app/o/[organizationId]/settings/components/integrations/add-organization-source-modal'
import { organizationSearchStatusLabel } from '@/app/o/[organizationId]/settings/components/integrations/organization-search-status'
import { OrganizationSlackAccountSetup } from '@/app/o/[organizationId]/settings/components/integrations/slack-account-setup'
import { IntegrationTile } from '@/app/workspace/[workspaceId]/integrations/components/integrations-showcase'
import { SearchSourceSetup } from '@/app/workspace/[workspaceId]/search/components/search-source-setup'
+import {
+ searchSetupAccessParam,
+ searchSetupParam,
+} from '@/app/workspace/[workspaceId]/search/search-params'
import {
SettingsEmptyState,
SettingsQueryErrorState,
@@ -30,25 +32,62 @@ import { usePermissionConfig } from '@/hooks/use-permission-config'
export function OrganizationIntegrationsSetup() {
const { organization, viewer, searchAccess } = useOrganizationContext()
const [search, setSearch] = useSettingsSearch()
+ const [setup, setSetup] = useQueryStates(
+ {
+ [searchSetupParam.key]: searchSetupParam.parser,
+ [searchSetupAccessParam.key]: searchSetupAccessParam.parser,
+ },
+ { history: 'replace' }
+ )
const overview = useOrganizationSearchOverview(organization.id, { enabled: viewer.isAdmin })
const availability = usePermissionConfig()
const approval = useUpdateSearchIntegration()
- const [deactivating, setDeactivating] = useState(null)
const providers = new Map(
overview.data?.providers.map((provider) => [provider.connectorType, provider])
)
+ const sources = SEARCH_SOURCE_TYPES.map(([type, meta]) => ({
+ type,
+ meta,
+ access: getConnectorAccessAvailability(meta, availability.integrationAvailability, {
+ memberAccessAvailable: searchAccess.memberScoped,
+ mirroredAccessAvailable: searchAccess.sourceMirrored,
+ oauthServiceAvailability: availability.oauthServiceAvailability,
+ isIntegrationAvailabilityReady: availability.isIntegrationAvailabilityReady,
+ }),
+ }))
const query = search.trim().toLowerCase()
- const visible = SEARCH_SOURCE_TYPES.filter(([, meta]) => meta.name.toLowerCase().includes(query))
- const deactivatingName = SEARCH_SOURCE_TYPES.find(([type]) => type === deactivating)?.[1].name
- const changeApproval = (connectorType: string, approved: boolean) => {
- approval.reset()
- if (!approved && (providers.get(connectorType)?.sourceCount ?? 0) > 0) {
- setDeactivating(connectorType)
+ const visible = sources.flatMap((source) => {
+ const provider = providers.get(source.type)
+ return provider &&
+ (provider.approved || provider.sourceCount > 0) &&
+ source.meta.name.toLowerCase().includes(query)
+ ? [{ ...source, provider }]
+ : []
+ })
+ const ready = Boolean(
+ !overview.isPending &&
+ !overview.isError &&
+ availability.isIntegrationAvailabilityReady &&
+ !availability.integrationAvailabilityError
+ )
+ const closePicker = () => {
+ if (!approval.isPending) void setSetup({ addConnector: null, 'source-access': null })
+ }
+ const selectSource = (type: string, accessMode: 'admin' | 'members') => {
+ const selectedType = searchSetupParam.parser.parse(type)
+ if (!selectedType || !ready || approval.isPending) return
+ const startSetup = () =>
+ void setSetup({
+ addConnector: selectedType,
+ 'source-access': accessMode === 'members' ? 'members' : null,
+ })
+ if (providers.get(type)?.approved) {
+ startSetup()
return
}
approval.mutate(
- { organizationId: organization.id, connectorType, approved },
- { onError: (error) => toast.error(error.message) }
+ { organizationId: organization.id, connectorType: type, approved: true },
+ { onSuccess: startSetup, onError: (error) => toast.error(error.message) }
)
}
if (!viewer.isAdmin) return null
@@ -59,113 +98,94 @@ export function OrganizationIntegrationsSetup() {
)
+ const feedback = overview.isError ? (
+ void overview.refetch()}
+ variant='inline'
+ />
+ ) : availability.integrationAvailabilityError ? (
+ void availability.refetchIntegrationAvailability()}
+ variant='inline'
+ />
+ ) : null
+
return (
-
- {availability.integrationAvailabilityError && (
- void availability.refetchIntegrationAvailability()}
- variant='inline'
- />
- )}
-
- {overview.isError ? (
-
void overview.refetch()}
- variant='inline'
- />
- ) : overview.isPending ? (
- Loading sources…
- ) : visible.length === 0 ? (
- No matching sources
- ) : (
- visible.map(([type, meta]) => {
- const provider = providers.get(type)
- const approved = provider?.approved === true
- const sourceCount = provider?.sourceCount ?? 0
- const access = getConnectorAccessAvailability(
- meta,
- availability.integrationAvailability,
- {
- memberAccessAvailable: searchAccess.memberScoped,
- mirroredAccessAvailable: searchAccess.sourceMirrored,
- oauthServiceAvailability: availability.oauthServiceAvailability,
- isIntegrationAvailabilityReady: availability.isIntegrationAvailabilityReady,
- }
- )
- const available = access.admin || access.members
- const hasSources = sourceCount > 0
- const manage = hasSources || canConnectWithDefaults(meta)
- let description = provider ? organizationSearchStatusLabel(provider) : undefined
- if (!hasSources && availability.isIntegrationAvailabilityReady && !available)
- description = 'Unavailable in this deployment'
- return (
- }
- title={meta.name}
- description={description}
- trailing={
-
- {(hasSources || (approved && available)) && (
-
- {manage ? 'Manage' : 'Set up'}
-
- )}
- changeApproval(type, checked)}
- />
-
- }
- />
- )
- })
- )}
-
- {
- if (!open && !approval.isPending) setDeactivating(null)
- }}
- title={`Deactivate ${deactivatingName ?? 'integration'}?`}
- text='Its content will be unavailable in Search, Assistant, and MCP. Sources and connected accounts are preserved.'
- confirm={{
- label: 'Deactivate',
- variant: 'destructive',
- pending: approval.isPending,
- onClick: () => {
- if (!deactivating) return
- approval.mutate(
- { organizationId: organization.id, connectorType: deactivating, approved: false },
- { onSuccess: () => setDeactivating(null) }
- )
+ <>
+ void setSetup({ addConnector: '', 'source-access': null }),
},
- }}
+ ]}
+ search={{ value: search, onChange: setSearch, placeholder: 'Search sources...' }}
>
- {approval.error?.message}
-
-
+ {setup.addConnector !== '' && feedback}
+
+ {overview.isError ? null : overview.isPending ? (
+ Loading sources…
+ ) : visible.length === 0 ? (
+
+ {query ? 'No matching sources' : 'No sources yet. Add a source to get started.'}
+
+ ) : (
+ visible.map(({ type, meta, access, provider }) => {
+ const available = access.admin || access.members
+ const status =
+ provider.approved && ready && !available
+ ? 'Unavailable in this deployment'
+ : organizationSearchStatusLabel(provider)
+ return (
+ }
+ title={meta.name}
+ description={[
+ status,
+ provider.sourceCount > 0
+ ? `${provider.sourceCount} ${provider.sourceCount === 1 ? 'connection' : 'connections'}`
+ : undefined,
+ ]
+ .filter(Boolean)
+ .join(' · ')}
+ href={organizationRoutes(organization.id).searchProvider(type)}
+ clickLabel={`Manage ${meta.name}`}
+ navigable
+ />
+ )
+ })
+ )}
+
+
+ {setup.addConnector === '' ? (
+
+ ) : (
+
+ )}
-
+ >
)
}
diff --git a/apps/sim/app/o/[organizationId]/settings/components/integrations/organization-search-stats-period.test.tsx b/apps/sim/app/o/[organizationId]/settings/components/integrations/organization-search-stats-period.test.tsx
new file mode 100644
index 00000000000..c5b62c0626c
--- /dev/null
+++ b/apps/sim/app/o/[organizationId]/settings/components/integrations/organization-search-stats-period.test.tsx
@@ -0,0 +1,127 @@
+/** @vitest-environment jsdom */
+import { act } from 'react'
+import { createRoot, type Root } from 'react-dom/client'
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import { OrganizationSearchStatsPeriod } from '@/app/o/[organizationId]/settings/components/integrations/organization-search-stats-period'
+
+let root: Root
+let container: HTMLDivElement
+let originalScroll: typeof HTMLElement.prototype.scrollIntoView
+const onChange = vi.fn()
+
+beforeEach(() => {
+ vi.clearAllMocks()
+ vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true)
+ originalScroll = HTMLElement.prototype.scrollIntoView
+ HTMLElement.prototype.scrollIntoView = vi.fn()
+ container = document.createElement('div')
+ document.body.appendChild(container)
+ root = createRoot(container)
+})
+afterEach(async () => {
+ await act(async () => root.unmount())
+ container.remove()
+ HTMLElement.prototype.scrollIntoView = originalScroll
+ vi.unstubAllGlobals()
+})
+
+async function render() {
+ await act(async () =>
+ root.render(
+
+ )
+ )
+}
+async function selectPeriod(label: string) {
+ const trigger = container.querySelector('[role="combobox"]')
+ expect(trigger).not.toBeNull()
+ await act(async () =>
+ trigger?.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }))
+ )
+ const option = Array.from(document.querySelectorAll('[role="option"]')).find(
+ (item) => item.textContent === label
+ )
+ expect(option).toBeDefined()
+ await act(async () => option?.dispatchEvent(new MouseEvent('mousedown', { bubbles: true })))
+}
+async function click(label: string) {
+ const button = Array.from(document.querySelectorAll('button')).find(
+ (item) => item.textContent === label
+ )
+ expect(button).toBeDefined()
+ await act(async () => button?.click())
+}
+
+describe('Stats date selection', () => {
+ it('keeps the existing range when calendar edits are cancelled', async () => {
+ await render()
+ await selectPeriod('Custom range')
+ expect(
+ document
+ .querySelector('[data-radix-popper-content-wrapper]')
+ ?.contains(document.activeElement)
+ ).toBe(true)
+ await click('5')
+ await click('8')
+ expect(onChange).not.toHaveBeenCalled()
+ await click('Cancel')
+ await vi.waitFor(() =>
+ expect(document.activeElement).toBe(container.querySelector('[role="combobox"]'))
+ )
+ expect(onChange).not.toHaveBeenCalled()
+ expect(document.body.textContent).not.toContain('Apply')
+ expect(container.textContent).toContain('Sep 1 – Sep 3')
+ })
+ it('applies both date-only boundaries in one update and closes the calendar', async () => {
+ await render()
+ await selectPeriod('Custom range')
+ await click('5')
+ await click('8')
+ await click('Apply')
+ expect(onChange).toHaveBeenCalledExactlyOnceWith({
+ period: 'custom',
+ startDate: '2024-09-05',
+ endDate: '2024-09-08',
+ })
+ expect(document.body.textContent).not.toContain('Apply')
+ })
+ it('clears custom boundaries when choosing a preset', async () => {
+ await render()
+ await selectPeriod('Past 3 days')
+ expect(onChange).toHaveBeenCalledExactlyOnceWith({
+ period: '3d',
+ startDate: null,
+ endDate: null,
+ })
+ })
+ it('opens the calendar from the keyboard and restores focus on Escape', async () => {
+ await render()
+ const trigger = container.querySelector('[role="combobox"]')
+ await act(async () => {
+ trigger?.focus()
+ trigger?.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }))
+ })
+ for (let index = 0; index < 6; index++) {
+ await act(async () =>
+ trigger?.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }))
+ )
+ }
+ await act(async () =>
+ trigger?.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))
+ )
+ expect(document.body.textContent).toContain('Apply')
+ expect(document.activeElement).not.toBe(trigger)
+ await act(async () =>
+ document.activeElement?.dispatchEvent(
+ new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })
+ )
+ )
+ await vi.waitFor(() => expect(document.activeElement).toBe(trigger))
+ expect(onChange).not.toHaveBeenCalled()
+ })
+})
diff --git a/apps/sim/app/o/[organizationId]/settings/components/integrations/organization-search-stats-period.tsx b/apps/sim/app/o/[organizationId]/settings/components/integrations/organization-search-stats-period.tsx
new file mode 100644
index 00000000000..cff874b2785
--- /dev/null
+++ b/apps/sim/app/o/[organizationId]/settings/components/integrations/organization-search-stats-period.tsx
@@ -0,0 +1,89 @@
+'use client'
+
+import { useRef, useState } from 'react'
+import { Calendar, ChipCombobox, Popover, PopoverAnchor, PopoverContent, toast } from '@sim/emcn'
+import { formatDateShort } from '@/lib/core/utils/date-display'
+import { getSearchStatsRangeError, type SEARCH_STATS_PERIODS } from '@/lib/knowledge/search/stats'
+
+const PERIOD_OPTIONS = [
+ { value: 'today', label: 'Today' },
+ { value: '3d', label: 'Past 3 days' },
+ { value: '7d', label: 'Past 7 days' },
+ { value: '14d', label: 'Past 14 days' },
+ { value: '30d', label: 'Past 30 days' },
+ { value: '90d', label: 'Past 90 days' },
+ { value: 'custom', label: 'Custom range' },
+] satisfies { value: (typeof SEARCH_STATS_PERIODS)[number]; label: string }[]
+
+interface SearchStatsPeriodSelection {
+ period: (typeof SEARCH_STATS_PERIODS)[number]
+ startDate: string | null
+ endDate: string | null
+}
+
+interface OrganizationSearchStatsPeriodProps extends SearchStatsPeriodSelection {
+ onChange: (selection: SearchStatsPeriodSelection) => void
+}
+
+export function OrganizationSearchStatsPeriod({
+ period,
+ startDate,
+ endDate,
+ onChange,
+}: OrganizationSearchStatsPeriodProps) {
+ const triggerContainerRef = useRef(null)
+ const calendarRef = useRef(null)
+ const [calendarOpen, setCalendarOpen] = useState(false)
+ const label =
+ period === 'custom' && startDate && endDate && !getSearchStatsRangeError({ startDate, endDate })
+ ? `${formatDateShort(startDate)} – ${formatDateShort(endDate)}`
+ : PERIOD_OPTIONS.find((option) => option.value === period)?.label
+
+ return (
+
+
{
+ const selected = PERIOD_OPTIONS.find((option) => option.value === value)
+ if (!selected) return
+ if (selected.value === 'custom') setCalendarOpen(true)
+ else onChange({ period: selected.value, startDate: null, endDate: null })
+ }}
+ />
+
+
+ calendarRef.current?.focus()}
+ onCloseAutoFocus={() =>
+ triggerContainerRef.current?.querySelector('[role="combobox"]')?.focus()
+ }
+ >
+ setCalendarOpen(false)}
+ onRangeChange={(start, end) => {
+ const error = getSearchStatsRangeError({ startDate: start, endDate: end })
+ if (error) {
+ toast.error(error)
+ return
+ }
+ onChange({ period: 'custom', startDate: start, endDate: end })
+ setCalendarOpen(false)
+ }}
+ />
+
+
+
+ )
+}
diff --git a/apps/sim/app/o/[organizationId]/settings/components/integrations/organization-search-status.test.ts b/apps/sim/app/o/[organizationId]/settings/components/integrations/organization-search-status.test.ts
index dc1bfa33cfa..90b069f5f44 100644
--- a/apps/sim/app/o/[organizationId]/settings/components/integrations/organization-search-status.test.ts
+++ b/apps/sim/app/o/[organizationId]/settings/components/integrations/organization-search-status.test.ts
@@ -16,13 +16,13 @@ describe('organization source status labels', () => {
it('describes the next step instead of calling all empty integrations unconfigured', () => {
expect(organizationSearchStatusLabel(provider)).toBe('Waiting for connections')
expect(organizationSearchStatusLabel({ ...provider, status: 'needs_setup' })).toBe(
- 'Source not configured'
+ 'Setup required'
)
expect(
organizationSearchStatusLabel({ ...provider, status: 'needs_setup', sourceCount: 1 })
).toBe('Waiting for first sync')
expect(organizationSearchStatusLabel({ ...provider, status: 'active', sourceCount: 1 })).toBe(
- 'Enabled'
+ 'Ready to search'
)
})
it.each([
@@ -50,6 +50,6 @@ describe('organization source status labels', () => {
status: 'needs_attention',
issue: 'sync_failed',
})
- ).toBe('Disabled')
+ ).toBe('Deactivated')
})
})
diff --git a/apps/sim/app/o/[organizationId]/settings/components/integrations/organization-search-status.ts b/apps/sim/app/o/[organizationId]/settings/components/integrations/organization-search-status.ts
index 547f0339812..5988b08ce56 100644
--- a/apps/sim/app/o/[organizationId]/settings/components/integrations/organization-search-status.ts
+++ b/apps/sim/app/o/[organizationId]/settings/components/integrations/organization-search-status.ts
@@ -1,16 +1,16 @@
import type { OrganizationSearchProviderSummary } from '@/lib/api/contracts/knowledge/connectors'
const STATUS_LABELS: Record = {
- needs_setup: 'Source not configured',
+ needs_setup: 'Setup required',
waiting_for_connections: 'Waiting for connections',
indexing: 'Indexing',
needs_attention: 'Sync failed',
paused: 'Paused',
- active: 'Enabled',
+ active: 'Ready to search',
}
export function organizationSearchStatusLabel(provider: OrganizationSearchProviderSummary): string {
- if (!provider.approved) return 'Disabled'
+ if (!provider.approved) return 'Deactivated'
if (provider.status === 'needs_setup' && provider.sourceCount > 0) return 'Waiting for first sync'
if (provider.status === 'needs_attention') {
const error =
diff --git a/apps/sim/app/o/[organizationId]/settings/components/integrations/organization-source-people.tsx b/apps/sim/app/o/[organizationId]/settings/components/integrations/organization-source-people.tsx
new file mode 100644
index 00000000000..338ba91c5d9
--- /dev/null
+++ b/apps/sim/app/o/[organizationId]/settings/components/integrations/organization-source-people.tsx
@@ -0,0 +1,79 @@
+'use client'
+
+import type { ComponentProps, ReactNode } from 'react'
+import { ChipSelect } from '@sim/emcn'
+import { useQueryState } from 'nuqs'
+import type { CredentialGroupOption } from '@/lib/api/contracts/credential-groups'
+import { getCredentialGroupIndexingConnector } from '@/lib/credential-groups/indexing'
+import { organizationPeopleIntegrationParam } from '@/app/o/[organizationId]/settings/components/integrations/search-params'
+import { OrganizationAccountPeople } from '@/ee/credential-groups/components/organization-account-people'
+
+interface OrganizationSourcePeopleProps
+ extends Omit, 'searchConnection' | 'filters'> {
+ options: CredentialGroupOption[]
+ tabs: ReactNode
+}
+
+export function OrganizationSourcePeople({
+ options,
+ tabs,
+ ...props
+}: OrganizationSourcePeopleProps) {
+ const [integration, setIntegration] = useQueryState(
+ organizationPeopleIntegrationParam.key,
+ organizationPeopleIntegrationParam.parser
+ )
+ const integrations = options
+ .flatMap((option) => {
+ const connector = getCredentialGroupIndexingConnector(option.provider)
+ return option.status === 'active' && connector
+ ? [
+ {
+ optionId: option.id,
+ type: connector.type,
+ name: connector.meta.name,
+ icon: connector.meta.icon,
+ needsSetup: option.provider === 'slack' && option.configurationStatus !== 'ready',
+ },
+ ]
+ : []
+ })
+ .sort((a, b) => a.name.localeCompare(b.name))
+ const selected = integrations.find((item) => item.type === integration)
+
+ return (
+
+
+ {tabs}
+ void setIntegration(value === 'all' ? null : value)}
+ disabled={props.enabled === false || Boolean(props.setupFallback)}
+ options={[
+ { value: 'all', label: 'All integrations' },
+ ...integrations.map((item) => ({
+ value: item.type,
+ label: item.name,
+ icon: item.icon,
+ })),
+ ]}
+ />
+
+ {selected?.needsSetup && (
+
+ Update the Slack app from Sources before requesting connections.
+
+ )}
+
+ }
+ />
+ )
+}
diff --git a/apps/sim/app/o/[organizationId]/settings/components/integrations/organization-source-stats.test.tsx b/apps/sim/app/o/[organizationId]/settings/components/integrations/organization-source-stats.test.tsx
new file mode 100644
index 00000000000..11481d6a1f4
--- /dev/null
+++ b/apps/sim/app/o/[organizationId]/settings/components/integrations/organization-source-stats.test.tsx
@@ -0,0 +1,152 @@
+/** @vitest-environment jsdom */
+import { act } from 'react'
+import { NuqsTestingAdapter } from 'nuqs/adapters/testing'
+import { createRoot, type Root } from 'react-dom/client'
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import type { OrganizationSearchStats } from '@/lib/api/contracts/knowledge/search-stats'
+
+const mocks = vi.hoisted(() => ({
+ query: vi.fn(),
+ chart: vi.fn(),
+ refetch: vi.fn(),
+ updateUrl: vi.fn(),
+}))
+vi.mock('@/hooks/queries/organization-search-stats', () => ({
+ useOrganizationSearchStats: mocks.query,
+}))
+vi.mock('@/components/charts', () => ({
+ BarChart: (props: unknown) => {
+ mocks.chart(props)
+ return Daily chart
+ },
+}))
+vi.mock('@/connectors/registry', () => ({
+ CONNECTOR_META_REGISTRY: { confluence: { name: 'Confluence' }, jira: { name: 'Jira' } },
+}))
+vi.mock('next/navigation', () => ({ useRouter: () => ({}) }))
+
+import { SettingsHeaderProvider } from '@/components/settings/settings-header'
+import { OrganizationSourceStats } from '@/app/o/[organizationId]/settings/components/integrations/organization-source-stats'
+
+const data: OrganizationSearchStats = {
+ start: '2026-09-04T00:00:00.000Z',
+ end: '2026-09-10T20:00:00.000Z',
+ totals: { invocations: 5, activePeople: 2, results: 8 },
+ series: [{ timestamp: '2026-09-10T00:00:00.000Z', invocations: 5 }],
+ sources: [{ sourceType: 'confluence', invocations: 4 }],
+ surfaces: [
+ { surface: 'dashboard', invocations: 3 },
+ { surface: 'mcp', invocations: 2 },
+ ],
+ people: [
+ {
+ userId: 'person',
+ name: 'Alex',
+ email: 'alex@example.com',
+ invocations: 5,
+ sourceTypes: ['confluence', 'jira'],
+ },
+ ],
+}
+let root: Root
+let container: HTMLDivElement
+
+beforeEach(() => {
+ vi.clearAllMocks()
+ vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true)
+ mocks.query.mockReturnValue({ data, isError: false, refetch: mocks.refetch })
+ container = document.createElement('div')
+ document.body.appendChild(container)
+ root = createRoot(container)
+})
+afterEach(async () => {
+ await act(async () => root.unmount())
+ container.remove()
+ vi.unstubAllGlobals()
+})
+async function render(searchParams = '') {
+ await act(async () =>
+ root.render(
+
+
+ Sources People Stats} />
+
+
+ )
+ )
+}
+
+describe('organization Search stats', () => {
+ it('renders request metrics, source labels, people and the canonical daily chart', async () => {
+ await render()
+ expect(container.textContent).not.toContain('No-result rate')
+ expect(container.textContent).toContain('Confluence, Jira')
+ expect(container.textContent).toContain('alex@example.com')
+ expect(mocks.chart).toHaveBeenCalledWith(
+ expect.objectContaining({ data: [{ timestamp: data.series[0].timestamp, value: 5 }] })
+ )
+ expect(container.textContent).not.toMatch(/citations|clicks|agent runs/i)
+ })
+ it('restores filters from the URL and scopes every metric request consistently', async () => {
+ await render('?stats-period=7d&stats-surface=mcp')
+ expect(mocks.query).toHaveBeenLastCalledWith({
+ organizationId: 'org',
+ period: '7d',
+ surface: 'mcp',
+ })
+ expect(container.textContent).not.toContain('Invocations by surface')
+ const filter = container.querySelector('button[aria-label="Search surface"]')
+ const toolbar = filter?.parentElement?.parentElement
+ expect(toolbar?.textContent).toContain('Sources People Stats')
+ })
+ it('restores a custom UTC range with the surface filter', async () => {
+ await render(
+ '?stats-period=custom&stats-start=2026-09-01&stats-end=2026-09-03&stats-surface=slack'
+ )
+ expect(mocks.query).toHaveBeenLastCalledWith({
+ organizationId: 'org',
+ period: 'custom',
+ startDate: '2026-09-01',
+ endDate: '2026-09-03',
+ surface: 'slack',
+ })
+ expect(container.textContent).toContain('Sep 1 – Sep 3')
+ })
+ it('keeps the daily chart mounted with zero-valued buckets when there is no activity', async () => {
+ mocks.query.mockReturnValue({
+ data: {
+ ...data,
+ totals: { invocations: 0, activePeople: 0, results: 0 },
+ series: data.series.map((point) => ({ ...point, invocations: 0 })),
+ sources: [],
+ surfaces: [],
+ people: [],
+ },
+ isError: false,
+ })
+ await render()
+ expect(container.textContent).not.toContain('No recorded Search activity')
+ expect(container.querySelectorAll('dd')).toHaveLength(3)
+ expect(Array.from(container.querySelectorAll('dd'), (metric) => metric.textContent)).toEqual([
+ '0',
+ '0',
+ '0',
+ ])
+ expect(mocks.chart).toHaveBeenCalledWith(
+ expect.objectContaining({
+ data: [{ timestamp: data.series[0].timestamp, value: 0 }],
+ })
+ )
+ })
+ it('offers retry instead of displaying stale successful totals on failure', async () => {
+ mocks.query.mockReturnValue({ data, isError: true, refetch: mocks.refetch })
+ await render()
+ expect(container.textContent).toContain('Couldn’t load Search stats')
+ expect(container.textContent).not.toContain('Search invocations')
+ const retry = Array.from(container.querySelectorAll('button')).find(
+ (button) => button.textContent === 'Try again'
+ )
+ await act(async () => retry?.click())
+ expect(mocks.refetch).toHaveBeenCalledOnce()
+ })
+})
diff --git a/apps/sim/app/o/[organizationId]/settings/components/integrations/organization-source-stats.tsx b/apps/sim/app/o/[organizationId]/settings/components/integrations/organization-source-stats.tsx
new file mode 100644
index 00000000000..1765187b12d
--- /dev/null
+++ b/apps/sim/app/o/[organizationId]/settings/components/integrations/organization-source-stats.tsx
@@ -0,0 +1,227 @@
+'use client'
+
+import { type ReactNode, useMemo } from 'react'
+import { Chip, ChipSelect, Tooltip } from '@sim/emcn'
+import { CircleInfo } from '@sim/emcn/icons'
+import { useQueryStates } from 'nuqs'
+import { BarChart } from '@/components/charts'
+import {
+ SEARCH_STATS_PEOPLE_LIMIT,
+ SEARCH_STATS_SURFACE_LABELS,
+ SEARCH_STATS_SURFACES,
+} from '@/lib/knowledge/search/stats'
+import { OrganizationSearchStatsPeriod } from '@/app/o/[organizationId]/settings/components/integrations/organization-search-stats-period'
+import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
+import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel'
+import {
+ RESOURCE_LIST_STACK,
+ SettingsResourceRow,
+} from '@/app/workspace/[workspaceId]/settings/components/settings-resource-row'
+import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section'
+import { CONNECTOR_META_REGISTRY } from '@/connectors/registry'
+import {
+ organizationSearchStatsParsers,
+ organizationSearchStatsUrlOptions,
+} from '@/ee/organization-search-stats/search-params'
+import { useOrganizationSearchStats } from '@/hooks/queries/organization-search-stats'
+
+const SURFACE_OPTIONS = [
+ { value: 'all', label: 'All surfaces' },
+ ...SEARCH_STATS_SURFACES.map((surface) => ({
+ value: surface,
+ label: SEARCH_STATS_SURFACE_LABELS[surface],
+ })),
+]
+
+interface OrganizationSourceStatsProps {
+ organizationId: string
+ tabs?: ReactNode
+}
+
+function sourceLabel(sourceType: string) {
+ return (
+ CONNECTOR_META_REGISTRY[sourceType]?.name ?? (sourceType === 'uploads' ? 'Uploads' : sourceType)
+ )
+}
+
+export function OrganizationSourceStats({ organizationId, tabs }: OrganizationSourceStatsProps) {
+ const [{ period, surface, startDate, endDate }, setFilters] = useQueryStates(
+ organizationSearchStatsParsers,
+ organizationSearchStatsUrlOptions
+ )
+ const stats = useOrganizationSearchStats({
+ organizationId,
+ period,
+ surface: surface ?? undefined,
+ ...(period === 'custom'
+ ? { startDate: startDate ?? undefined, endDate: endDate ?? undefined }
+ : {}),
+ })
+ const series = useMemo(
+ () =>
+ stats.data?.series.map((point) => ({
+ timestamp: point.timestamp,
+ value: point.invocations,
+ })) ?? [],
+ [stats.data?.series]
+ )
+ const data = stats.data
+ const totals = data?.totals
+ const metrics = totals
+ ? [
+ { label: 'Search invocations', value: totals.invocations.toLocaleString() },
+ { label: 'Active people', value: totals.activePeople.toLocaleString() },
+ { label: 'Results returned', value: totals.results.toLocaleString() },
+ ]
+ : []
+
+ return (
+
+
+ {tabs}
+
+
+ void setFilters({ surface: organizationSearchStatsParsers.surface.parse(value) })
+ }
+ />
+ void setFilters(selection)}
+ />
+
+
+ {stats.isError ? (
+
+ Couldn’t load Search stats. void stats.refetch()}>Try again
+
+ ) : !data || !totals ? (
+ Loading Search stats…
+ ) : (
+ <>
+
+ {metrics.map((metric) => (
+
+
{metric.label}
+ {metric.value}
+
+ ))}
+
+
+
+
+
+
+ Successful Search requests since tracking was enabled. Assistant and MCP counts
+ are Search tool calls. Results count each document once per request. One request
+ can return multiple sources.
+
+
+ }
+ action={
+
+ {period === 'custom' ? 'UTC' : 'UTC · Includes today'}
+
+ }
+ >
+
+
+ {!surface && totals.invocations > 0 && (
+
+
+ {data.surfaces.map((row) => (
+
+ {row.invocations.toLocaleString()}
+
+ }
+ />
+ ))}
+
+
+ )}
+
+ Invocations returning this source
+
+ }
+ >
+ {data.sources.length ? (
+
+ {data.sources.map((row) => {
+ const Icon = CONNECTOR_META_REGISTRY[row.sourceType]?.icon
+ return (
+ : undefined}
+ title={sourceLabel(row.sourceType)}
+ badge={
+
+ {row.invocations.toLocaleString()}
+
+ }
+ />
+ )
+ })}
+
+ ) : (
+
+ No sources returned in this period.
+
+ )}
+
+
+ Top {SEARCH_STATS_PEOPLE_LIMIT} · Invocations
+
+ }
+ >
+ {data.people.length ? (
+
+ {data.people.map((person) => (
+
+ {person.invocations.toLocaleString()}
+
+ }
+ />
+ ))}
+
+ ) : (
+
+ No active people in this period.
+
+ )}
+
+ >
+ )}
+
+ )
+}
diff --git a/apps/sim/app/o/[organizationId]/settings/components/integrations/search-params.ts b/apps/sim/app/o/[organizationId]/settings/components/integrations/search-params.ts
index 30d5f4c5561..ad97007ca37 100644
--- a/apps/sim/app/o/[organizationId]/settings/components/integrations/search-params.ts
+++ b/apps/sim/app/o/[organizationId]/settings/components/integrations/search-params.ts
@@ -1,8 +1,8 @@
-import { parseAsStringLiteral } from 'nuqs/server'
+import { createSerializer, parseAsString, parseAsStringLiteral } from 'nuqs/server'
export const organizationIntegrationsTabParam = {
key: 'tab',
- parser: parseAsStringLiteral(['providers', 'people']).withDefault('providers'),
+ parser: parseAsStringLiteral(['providers', 'people', 'stats']).withDefault('providers'),
} as const
export const connectedAccountsParam = {
@@ -10,7 +10,14 @@ export const connectedAccountsParam = {
parser: parseAsStringLiteral(['slack']),
} as const
-export const organizationProviderTabParam = {
- key: 'view',
- parser: parseAsStringLiteral(['sources', 'accounts']).withDefault('sources'),
+/** An absent integration includes connections from all integrations. */
+export const organizationPeopleIntegrationParam = {
+ key: 'integration',
+ parser: parseAsString,
} as const
+
+export const serializeOrganizationPeople = createSerializer({
+ tab: organizationIntegrationsTabParam.parser,
+ integration: organizationPeopleIntegrationParam.parser,
+ 'credential-group-people': parseAsString,
+})
diff --git a/apps/sim/app/o/[organizationId]/settings/components/integrations/slack-account-removal.tsx b/apps/sim/app/o/[organizationId]/settings/components/integrations/slack-account-removal.tsx
index 2dff4d17b5f..10dac1eb1bb 100644
--- a/apps/sim/app/o/[organizationId]/settings/components/integrations/slack-account-removal.tsx
+++ b/apps/sim/app/o/[organizationId]/settings/components/integrations/slack-account-removal.tsx
@@ -24,8 +24,8 @@ export function OrganizationSlackAccountRemoval({
onOpenChange={(open) => {
if (!open && !update.isPending) onClose()
}}
- title='Remove Slack account setup?'
- text='This disconnects your organization’s Slack accounts and clears their saved app configuration. Remove any sources using these accounts first.'
+ title='Remove Slack app setup?'
+ text='This disconnects your organization’s Slack accounts and clears their saved app configuration. Remove any connections using these accounts first.'
confirm={{
label: 'Remove',
variant: 'destructive',
diff --git a/apps/sim/app/o/[organizationId]/settings/components/integrations/slack-account-setup.test.tsx b/apps/sim/app/o/[organizationId]/settings/components/integrations/slack-account-setup.test.tsx
index 1cc0c9b77b9..eef37d52a2f 100644
--- a/apps/sim/app/o/[organizationId]/settings/components/integrations/slack-account-setup.test.tsx
+++ b/apps/sim/app/o/[organizationId]/settings/components/integrations/slack-account-setup.test.tsx
@@ -72,8 +72,57 @@ describe('organization Slack setup continuation', () => {
it('prepares a missing container without asking the admin to make an extra choice', async () => {
await render()
expect(mocks.ensure).toHaveBeenCalledExactlyOnceWith({ organizationId: 'org-a' })
- expect(document.body.textContent).toContain('Loading Slack setup')
- expect(document.body.textContent).not.toContain('Continue')
+ expect(document.querySelector('[role="dialog"]')).toBeNull()
+ expect(mocks.modal).not.toHaveBeenCalled()
+ mocks.prepare.mockReturnValue({ mutate: mocks.ensure, isIdle: false, isPending: true })
+ await render()
+ expect(document.querySelector('[role="dialog"]')).toBeNull()
+ expect(mocks.ensure).toHaveBeenCalledOnce()
+ mocks.prepare.mockReturnValue({
+ mutate: mocks.ensure,
+ isIdle: false,
+ isPending: false,
+ data: { credentialGroup: { id: 'group-a', organizationId: 'org-a', options: [] } },
+ })
+ await render()
+ expect(mocks.modal).toHaveBeenCalledWith(
+ expect.objectContaining({ credentialGroupId: 'group-a' }),
+ undefined
+ )
+ })
+
+ it('does not open a temporary modal while discovering accounts', async () => {
+ mocks.accounts.mockReturnValue({ isSuccess: false, isPending: true })
+ await render()
+ expect(document.querySelector('[role="dialog"]')).toBeNull()
+ expect(mocks.ensure).not.toHaveBeenCalled()
+ expect(mocks.modal).not.toHaveBeenCalled()
+ })
+
+ it.each(['discovery', 'preparation'])('keeps %s errors recoverable', async (stage) => {
+ const retry = vi.fn()
+ if (stage === 'discovery') {
+ mocks.accounts.mockReturnValue({
+ isSuccess: false,
+ error: new Error('Could not load Slack setup'),
+ refetch: retry,
+ })
+ } else {
+ mocks.prepare.mockReturnValue({
+ mutate: mocks.ensure,
+ isIdle: false,
+ error: new Error('Could not prepare Slack setup'),
+ })
+ }
+ await render()
+ expect(document.body.textContent).toContain('Set up Slack app')
+ const retryButton = Array.from(document.querySelectorAll('button')).find(
+ (button) => button.textContent === 'Try again'
+ )
+ expect(retryButton).toBeDefined()
+ await act(async () => retryButton?.click())
+ if (stage === 'discovery') expect(retry).toHaveBeenCalledOnce()
+ else expect(mocks.ensure).toHaveBeenCalledExactlyOnceWith({ organizationId: 'org-a' })
})
it('does not prepare accounts or open admin setup for an ordinary member', async () => {
diff --git a/apps/sim/app/o/[organizationId]/settings/components/integrations/slack-account-setup.tsx b/apps/sim/app/o/[organizationId]/settings/components/integrations/slack-account-setup.tsx
index e5e46838ec2..37d11404811 100644
--- a/apps/sim/app/o/[organizationId]/settings/components/integrations/slack-account-setup.tsx
+++ b/apps/sim/app/o/[organizationId]/settings/components/integrations/slack-account-setup.tsx
@@ -15,10 +15,7 @@ import {
searchSetupParam,
searchSetupReturnParam,
} from '@/app/workspace/[workspaceId]/search/search-params'
-import {
- SettingsEmptyState,
- SettingsQueryErrorState,
-} from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
+import { SettingsQueryErrorState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
import { SlackManagedUsersModal } from '@/ee/credential-groups/components/slack-managed-users-modal'
import {
useEnsureOrganizationAccounts,
@@ -83,32 +80,30 @@ export function OrganizationSlackAccountSetup() {
}}
/>
)
+ const error = accounts.error ?? setupError
+ if (!error) return null
return (
{
if (!next) close()
}}
- srTitle='Connect Slack accounts'
+ srTitle='Set up Slack app'
>
- Connect Slack accounts
+ Set up Slack app
-
- {accounts.error || setupError ? (
-
- accounts.error
- ? void accounts.refetch()
- : ensureAccounts({ organizationId: organization.id })
- }
- variant='inline'
- />
- ) : (
- Loading Slack setup…
- )}
+
+
+ accounts.error
+ ? void accounts.refetch()
+ : ensureAccounts({ organizationId: organization.id })
+ }
+ variant='inline'
+ />
diff --git a/apps/sim/app/o/[organizationId]/settings/integrations/providers/[connectorType]/page.test.tsx b/apps/sim/app/o/[organizationId]/settings/integrations/providers/[connectorType]/page.test.tsx
index 25ff75fb3e8..bf09e5c23f9 100644
--- a/apps/sim/app/o/[organizationId]/settings/integrations/providers/[connectorType]/page.test.tsx
+++ b/apps/sim/app/o/[organizationId]/settings/integrations/providers/[connectorType]/page.test.tsx
@@ -1,105 +1,81 @@
/** @vitest-environment node */
-import { Suspense } from 'react'
-import { beforeEach, describe, expect, it, vi } from 'vitest'
+import { beforeEach, expect, it, vi } from 'vitest'
-const mocks = vi.hoisted(() => ({
- session: vi.fn(),
- authorize: vi.fn(),
- redirect: vi.fn(),
- notFound: vi.fn(),
-}))
+const mocks = vi.hoisted(() => ({ session: vi.fn(), authorize: vi.fn(), redirect: vi.fn() }))
vi.mock('@/lib/auth', () => ({ getSession: mocks.session }))
vi.mock('@/lib/settings/application/organization-section-access', () => ({
authorizeOrganizationSettingsSection: mocks.authorize,
}))
-vi.mock('@/lib/sim-search/connectors', () => ({
- SEARCH_SOURCE_TYPES: [['google_drive', { name: 'Google Drive' }]],
+vi.mock('next/navigation', () => ({
+ redirect: (url: string) => {
+ mocks.redirect(url)
+ throw new Error('redirect')
+ },
+ notFound: () => {
+ throw new Error('not found')
+ },
}))
-vi.mock('next/navigation', () => ({ redirect: mocks.redirect, notFound: mocks.notFound }))
-vi.mock('@/components/settings/settings-header', () => ({
- SettingsHeaderProvider: 'header-provider',
- SettingsHeaderShell: 'header-shell',
+vi.mock('@/lib/sim-search/connectors', () => ({
+ SEARCH_SOURCE_TYPES: [
+ ['jira', { name: 'Jira' }],
+ ['confluence', { name: 'Confluence' }],
+ ],
}))
vi.mock(
'@/app/o/[organizationId]/settings/integrations/providers/[connectorType]/provider-detail',
- () => ({ OrganizationProviderDetail: 'provider-detail' })
+ () => ({ OrganizationProviderDetail: () => null })
)
+vi.mock('@/app/workspace/[workspaceId]/settings/components/settings-empty-state', () => ({
+ SettingsEmptyState: () => null,
+}))
-import OrganizationProviderLayout from '@/app/o/[organizationId]/settings/integrations/providers/[connectorType]/layout'
-import OrganizationProviderPage, {
- generateMetadata,
-} from '@/app/o/[organizationId]/settings/integrations/providers/[connectorType]/page'
-
-const props = {
- params: Promise.resolve({ organizationId: 'org-one', connectorType: 'google_drive' }),
-}
-
-describe('organization provider page authorization', () => {
- beforeEach(() => {
- vi.clearAllMocks()
- mocks.session.mockResolvedValue({ user: { id: 'admin-one' } })
- mocks.authorize.mockResolvedValue(true)
- mocks.redirect.mockImplementation(() => {
- throw new Error('redirect')
- })
- mocks.notFound.mockImplementation(() => {
- throw new Error('not found')
- })
- })
+import OrganizationProviderPage from '@/app/o/[organizationId]/settings/integrations/providers/[connectorType]/page'
- it('checks the target organization integration policy before rendering provider management', async () => {
- const page = await OrganizationProviderPage(props)
- expect(mocks.authorize).toHaveBeenCalledWith({
- organizationId: 'org-one',
- userId: 'admin-one',
- section: 'integrations',
- })
- expect(page.type).toBe(Suspense)
- expect(page.props.children).toMatchObject({
- type: 'provider-detail',
- props: { connectorType: 'google_drive' },
- })
- })
+beforeEach(() => {
+ vi.clearAllMocks()
+ mocks.session.mockResolvedValue({ user: { id: 'admin-1' } })
+ mocks.authorize.mockResolvedValue(true)
+})
- it('rejects unknown or unsupported providers before loading protected organization state', async () => {
+it.each(['jira', 'confluence'])(
+ 'moves legacy %s Accounts links to filtered People and preserves the search',
+ async (connectorType) => {
await expect(
OrganizationProviderPage({
- params: Promise.resolve({ organizationId: 'org-one', connectorType: 'unknown-provider' }),
+ params: Promise.resolve({ organizationId: 'org-1', connectorType }),
+ searchParams: Promise.resolve({
+ view: 'accounts',
+ 'credential-group-people': 'alex+qa@example.com',
+ }),
})
- ).rejects.toThrow('not found')
- expect(mocks.session).not.toHaveBeenCalled()
- expect(mocks.authorize).not.toHaveBeenCalled()
- })
-
- it('does not render when the admin or feature gate denies access', async () => {
- mocks.authorize.mockResolvedValue(false)
- await expect(OrganizationProviderPage(props)).rejects.toThrow('not found')
- })
-
- it('returns signed-out users to the requested provider after login', async () => {
- mocks.session.mockResolvedValue(null)
- await expect(OrganizationProviderPage(props)).rejects.toThrow('redirect')
- const url = new URL(mocks.redirect.mock.calls[0][0], 'https://example.com')
- expect(url.pathname).toBe('/login')
- expect(url.searchParams.get('callbackUrl')).toBe(
- '/o/org-one/settings/integrations/providers/google_drive'
- )
- expect(mocks.authorize).not.toHaveBeenCalled()
- })
+ ).rejects.toThrow('redirect')
+ const url = new URL(mocks.redirect.mock.lastCall![0], 'https://example.com')
+ expect(url.pathname).toBe('/o/org-1/settings/integrations')
+ expect(url.searchParams.get('tab')).toBe('people')
+ expect(url.searchParams.get('integration')).toBe(connectorType)
+ expect(url.searchParams.get('credential-group-people')).toBe('alex+qa@example.com')
+ expect(url.searchParams.has('view')).toBe(false)
+ }
+)
- it('keeps the settings header mounted independently of provider authorization', () => {
- const layout = OrganizationProviderLayout({ children: Loading integration })
- expect(layout.type).toBe('header-provider')
- expect(layout.props.children).toMatchObject({
- type: 'header-shell',
- props: { meta: { title: 'Integration' } },
+it('authorizes organization settings before redirecting a legacy link', async () => {
+ mocks.authorize.mockResolvedValue(false)
+ await expect(
+ OrganizationProviderPage({
+ params: Promise.resolve({ organizationId: 'org-1', connectorType: 'jira' }),
+ searchParams: Promise.resolve({ view: 'accounts' }),
})
- expect(mocks.authorize).not.toHaveBeenCalled()
- })
-
- it('uses the supported provider name as page metadata', async () => {
- expect(await generateMetadata(props)).toEqual({ title: 'Google Drive' })
- expect(mocks.session).not.toHaveBeenCalled()
- expect(mocks.authorize).not.toHaveBeenCalled()
- })
+ ).rejects.toThrow('not found')
+ expect(mocks.redirect).not.toHaveBeenCalled()
})
+
+it.each(['jira', ''])(
+ 'preserves an active setup in a legacy Accounts link (%s)',
+ async (addConnector) => {
+ await OrganizationProviderPage({
+ params: Promise.resolve({ organizationId: 'org-1', connectorType: 'jira' }),
+ searchParams: Promise.resolve({ view: 'accounts', addConnector, 'source-access': 'members' }),
+ })
+ expect(mocks.redirect).not.toHaveBeenCalled()
+ }
+)
diff --git a/apps/sim/app/o/[organizationId]/settings/integrations/providers/[connectorType]/page.tsx b/apps/sim/app/o/[organizationId]/settings/integrations/providers/[connectorType]/page.tsx
index 8250da53e98..5f0762a0e49 100644
--- a/apps/sim/app/o/[organizationId]/settings/integrations/providers/[connectorType]/page.tsx
+++ b/apps/sim/app/o/[organizationId]/settings/integrations/providers/[connectorType]/page.tsx
@@ -6,11 +6,14 @@ import { organizationRoutes } from '@/lib/navigation/paths'
import { authorizeOrganizationSettingsSection } from '@/lib/settings/application/organization-section-access'
import { SEARCH_SOURCE_TYPES } from '@/lib/sim-search/connectors'
import { buildAuthCrossLink } from '@/app/(auth)/auth-redirect'
+import { serializeOrganizationPeople } from '@/app/o/[organizationId]/settings/components/integrations/search-params'
import { OrganizationProviderDetail } from '@/app/o/[organizationId]/settings/integrations/providers/[connectorType]/provider-detail'
+import { searchSetupParam } from '@/app/workspace/[workspaceId]/search/search-params'
import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
interface OrganizationProviderPageProps {
params: Promise<{ organizationId: string; connectorType: string }>
+ searchParams: Promise>
}
export async function generateMetadata({
@@ -22,7 +25,10 @@ export async function generateMetadata({
}
}
-export default async function OrganizationProviderPage({ params }: OrganizationProviderPageProps) {
+export default async function OrganizationProviderPage({
+ params,
+ searchParams,
+}: OrganizationProviderPageProps) {
const { organizationId, connectorType } = await params
if (!SEARCH_SOURCE_TYPES.some(([type]) => type === connectorType)) notFound()
const session = await getSession()
@@ -41,6 +47,26 @@ export default async function OrganizationProviderPage({ params }: OrganizationP
}))
)
notFound()
+ const query = await searchParams
+ const activeSetup =
+ typeof query.addConnector === 'string'
+ ? searchSetupParam.parser.parse(query.addConnector)
+ : null
+ if (query.view === 'accounts' && activeSetup === null) {
+ redirect(
+ serializeOrganizationPeople(
+ organizationRoutes(organizationId).settingsSection('integrations'),
+ {
+ tab: 'people',
+ integration: connectorType,
+ 'credential-group-people':
+ typeof query['credential-group-people'] === 'string'
+ ? query['credential-group-people']
+ : null,
+ }
+ )
+ )
+ }
return (
Loading integration…}
diff --git a/apps/sim/app/o/[organizationId]/settings/integrations/providers/[connectorType]/provider-detail.test.tsx b/apps/sim/app/o/[organizationId]/settings/integrations/providers/[connectorType]/provider-detail.test.tsx
index 7dda89df3ff..594914fb52f 100644
--- a/apps/sim/app/o/[organizationId]/settings/integrations/providers/[connectorType]/provider-detail.test.tsx
+++ b/apps/sim/app/o/[organizationId]/settings/integrations/providers/[connectorType]/provider-detail.test.tsx
@@ -116,6 +116,7 @@ vi.mock('@/app/o/[organizationId]/settings/components/integrations/slack-account
import { SettingsHeaderProvider, SettingsHeaderShell } from '@/components/settings/settings-header'
import { OrganizationProviderDetail } from '@/app/o/[organizationId]/settings/integrations/providers/[connectorType]/provider-detail'
+import { CONNECTOR_META_REGISTRY } from '@/connectors/registry'
const provider = {
connectorType: 'google_drive',
@@ -221,46 +222,21 @@ describe('organization provider management', () => {
}
it.each(['gmail', 'google_calendar', 'google_drive'])(
- 'lets %s wait for connections without requiring source setup',
+ 'offers configuration setup directly for %s without an Accounts or Advanced tab',
async (connectorType) => {
mocks.overview.mockReturnValue({
- data: {
- providers: [
- {
- connectorType,
- approved: true,
- status: 'waiting_for_connections',
- sourceCount: 0,
- issue: null,
- isSyncing: false,
- },
- ],
- },
+ data: { providers: [{ ...provider, connectorType, status: 'waiting_for_connections' }] },
})
- mocks.accounts.mockReturnValue({ data: { credentialGroup: null }, isPending: false })
mocks.sources.mockReturnValue({ data: [], isPending: false })
- await render(connectorType, '?view=accounts')
+ await render(connectorType)
expect(container.textContent).toContain('Waiting for connections')
expect(container.textContent).toContain(
- 'Members connect their accounts from Integrations. Indexing starts automatically.'
+ `No ${CONNECTOR_META_REGISTRY[connectorType]?.name} connections yet.`
)
- expect(container.textContent).not.toContain('Add source')
- expect(container.textContent).not.toContain('Add sync configuration')
- expect(container.querySelector('a[href="/o/org-one/integrations"]')).toBeNull()
- expect(container.textContent).not.toContain('Open Integrations')
- expect(mocks.sources).toHaveBeenCalledWith(
- expect.any(Object),
- expect.objectContaining({ enabled: false })
- )
- await click('Advanced')
- expect(container.textContent).toContain('No sync configurations yet.')
- expect(container.textContent).toContain('Add sync configuration')
- await click('Add sync configuration')
+ expect(container.querySelector('[role="radio"]')).toBeNull()
+ await click('Connect service account')
await vi.waitFor(() => {
- expect(mocks.updateUrl).toHaveBeenLastCalledWith(
- expect.objectContaining({ searchParams: expect.any(URLSearchParams) })
- )
- expect(mocks.updateUrl.mock.calls.at(-1)![0].searchParams.get('addConnector')).toBe(
+ expect(mocks.updateUrl.mock.calls.at(-1)?.[0].searchParams.get('addConnector')).toBe(
connectorType
)
})
@@ -284,7 +260,7 @@ describe('organization provider management', () => {
expect(container.textContent).not.toContain(
'Members connect their accounts from Integrations.'
)
- await click('Add sync configuration')
+ await click('Connect service account')
await vi.waitFor(() => {
const params = mocks.updateUrl.mock.calls.at(-1)![0].searchParams
expect(params.get('addConnector')).toBe(connectorType)
@@ -320,29 +296,17 @@ describe('organization provider management', () => {
])('opens configurations for the $name setup', async ({ modes }) => {
withSources(modes)
await render(connectorType)
- expect(container.querySelector('[role="radio"][aria-checked="true"]')).toHaveTextContent(
- 'Advanced'
- )
+ expect(container.querySelector('[role="radio"]')).toBeNull()
expect(mocks.sources).toHaveBeenLastCalledWith(
expect.any(Object),
expect.objectContaining({ enabled: true })
)
expect(mocks.accounts).toHaveBeenLastCalledWith(undefined)
- expect(container.textContent).toContain('Add sync configuration')
+ expect(container.textContent).toContain('Connect service account')
if (modes.length === 0)
- expect(container.textContent).toContain('No sync configurations yet.')
- })
-
- it.each(['accounts', 'sources'])('honors explicit %s links', async (view) => {
- withSources(['admin'])
- await render(connectorType, `?view=${view}`)
- expect(mocks.sources).toHaveBeenLastCalledWith(
- expect.any(Object),
- expect.objectContaining({ enabled: view === 'sources' })
- )
- expect(container.querySelector('[role="radio"][aria-checked="true"]')).toHaveTextContent(
- view === 'accounts' ? 'Accounts' : 'Advanced'
- )
+ expect(container.textContent).toContain(
+ `No ${CONNECTOR_META_REGISTRY[connectorType]?.name} connections yet.`
+ )
})
it('loads the configuration list in parallel with its overview and retains the default', async () => {
@@ -357,9 +321,7 @@ describe('organization provider management', () => {
)
withSources(['members'])
await render(connectorType)
- expect(container.querySelector('[role="radio"][aria-checked="true"]')).toHaveTextContent(
- 'Advanced'
- )
+ expect(container.querySelector('[role="radio"]')).toBeNull()
expect(container.textContent).toContain('Engineering handbook')
expect(mocks.sources).toHaveBeenLastCalledWith(
expect.any(Object),
@@ -367,43 +329,6 @@ describe('organization provider management', () => {
)
expect(mocks.accounts).toHaveBeenLastCalledWith(undefined)
})
-
- it('preserves an explicit Accounts choice when the overview changes', async () => {
- withSources(['members'])
- await render(connectorType)
- await click('Accounts')
- await vi.waitFor(() =>
- expect(mocks.updateUrl.mock.calls.at(-1)?.[0].searchParams.get('view')).toBe('accounts')
- )
- withSources(['admin'])
- await render(connectorType)
- expect(container.querySelector('[role="radio"][aria-checked="true"]')).toHaveTextContent(
- 'Accounts'
- )
- expect(mocks.sources).toHaveBeenLastCalledWith(
- expect.any(Object),
- expect.objectContaining({ enabled: false })
- )
- })
-
- it.each([
- { accessMode: 'admin', method: 'Service account' },
- { accessMode: 'members', method: 'Member accounts' },
- ])(
- 'identifies $method configurations without changing their title or destination',
- async ({ accessMode, method }) => {
- withSources([accessMode])
- mocks.sources.mockReturnValue({
- data: [{ ...source, connectorType, accessMode }],
- isPending: false,
- })
- await render(connectorType)
- expect(container.textContent).toContain(`${method} · Last synced`)
- expect(
- container.querySelector('a[aria-label="Open Engineering handbook"]')
- ).toHaveAttribute('href', '/o/org-one/settings/integrations/sources/source-one')
- }
- )
}
)
@@ -412,7 +337,15 @@ describe('organization provider management', () => {
async (status) => {
withSlackAccounts(true, status)
await render('slack')
- await click('Remove account setup')
+ const headerRemove = Array.from(container.querySelectorAll('button')).find(
+ (button) => button.textContent === 'Remove app setup'
+ )
+ const deactivate = Array.from(container.querySelectorAll('button')).find(
+ (button) => button.textContent === 'Deactivate'
+ )
+ expect(headerRemove?.className).toBe(deactivate?.className)
+ expect(headerRemove?.querySelector('[class*="text-error"]')).toBeNull()
+ await click('Remove app setup')
expect(mocks.removeAccounts).not.toHaveBeenCalled()
expect(document.querySelector('[role="dialog"]')).toHaveTextContent('saved app configuration')
await click('Remove')
@@ -434,7 +367,7 @@ describe('organization provider management', () => {
it('offers removal when Slack is deactivated and allows cancelling without a mutation', async () => {
withSlackAccounts(false)
await render('slack')
- await click('Remove account setup')
+ await click('Remove app setup')
await click('Cancel')
expect(document.querySelector('[role="dialog"]')).toBeNull()
expect(mocks.removeAccounts).not.toHaveBeenCalled()
@@ -444,32 +377,19 @@ describe('organization provider management', () => {
withSlackAccounts()
mocks.accountRemovalError = new Error('Remove the source using these accounts first.')
await render('slack')
- await click('Remove account setup')
+ await click('Remove app setup')
await click('Remove')
expect(document.querySelector('[role="dialog"] [role="alert"]')).toHaveTextContent(
'Remove the source using these accounts first.'
)
})
- it('passes the removal action to the Slack Accounts tab header', async () => {
- withSlackAccounts()
- await render('slack', '?view=accounts')
- const actions = mocks.people.mock.calls.at(-1)![0].panel.actions
- expect(actions).toEqual([
- expect.objectContaining({ text: 'Remove account setup', onSelect: expect.any(Function) }),
- ])
- await act(async () => actions[0].onSelect())
- expect(document.querySelector('[role="dialog"]')).toHaveTextContent(
- 'Remove Slack account setup?'
- )
- })
-
it('keeps Slack cleanup available even when personal source creation is unavailable', async () => {
withSlackAccounts()
mocks.personal = false
await render('slack')
expect(mocks.accounts).toHaveBeenCalledWith('org-one')
- await click('Remove account setup')
+ await click('Remove app setup')
expect(document.querySelector('[role="dialog"]')).not.toBeNull()
})
@@ -538,14 +458,12 @@ describe('organization provider management', () => {
expect(container.textContent).not.toContain('Activate this integration')
expect(mocks.people).not.toHaveBeenCalled()
expect(
- container.querySelector(
- `input[placeholder="${params === '?view=accounts' ? 'Search people...' : 'Search sync configurations...'}"]`
- )
+ container.querySelector('input[placeholder="Search Google Drive connections..."]')
).toBeEnabled()
}
)
- it('hides cached account content and retries when overview access is revoked', async () => {
+ it('hides cached source content and retries when overview access is revoked', async () => {
const refetch = vi.fn()
mocks.overview.mockReturnValue({
data: { providers: [provider] },
@@ -553,21 +471,29 @@ describe('organization provider management', () => {
error: new ApiClientError({ status: 403, message: 'Access denied', body: null }),
refetch,
})
- await render('google_drive', '?view=accounts&credential-group-people=alex')
+ await render('google_drive', '?search=alex')
expect(container.textContent).toContain('Access denied')
expect(mocks.people).not.toHaveBeenCalled()
- expect(container.querySelector('input[placeholder="Search people..."]')).toHaveValue('alex')
- expect(container.querySelector('input[placeholder="Search people..."]')).toBeEnabled()
+ expect(
+ container.querySelector('input[placeholder="Search Google Drive connections..."]')
+ ).toHaveValue('alex')
+ expect(
+ container.querySelector('input[placeholder="Search Google Drive connections..."]')
+ ).toBeEnabled()
await click('Try again')
expect(refetch).toHaveBeenCalledOnce()
})
- it('offers activation from the accounts tab when an existing provider is deactivated', async () => {
+ it('offers activation when an existing provider is deactivated', async () => {
mocks.overview.mockReturnValue({ data: { providers: [{ ...provider, approved: false }] } })
- await render('google_drive', '?view=accounts&credential-group-people=alex')
+ await render('google_drive', '?search=alex')
expect(mocks.people).not.toHaveBeenCalled()
- expect(container.querySelector('input[placeholder="Search people..."]')).toHaveValue('alex')
- expect(container.querySelector('input[placeholder="Search people..."]')).toBeEnabled()
+ expect(
+ container.querySelector('input[placeholder="Search Google Drive connections..."]')
+ ).toHaveValue('alex')
+ expect(
+ container.querySelector('input[placeholder="Search Google Drive connections..."]')
+ ).toBeEnabled()
await click('Activate')
expect(mocks.activate).toHaveBeenCalledWith({
organizationId: 'org-one',
@@ -576,7 +502,7 @@ describe('organization provider management', () => {
})
})
- it('shows activation failures on Accounts and keeps activation available for retry', async () => {
+ it('shows activation failures and keeps activation available for retry', async () => {
mocks.overview.mockReturnValue({ data: { providers: [{ ...provider, approved: false }] } })
mocks.approvalError = new Error('Activation could not be saved')
await render('google_drive', '?view=accounts')
@@ -590,56 +516,6 @@ describe('organization provider management', () => {
})
})
- it.each([
- ['loading', 'Loading accounts…'],
- ['error', 'Accounts unavailable'],
- [
- 'missing group',
- 'Members connect their accounts from Integrations. Indexing starts automatically.',
- ],
- [
- 'missing provider option',
- 'Members connect their accounts from Integrations. Indexing starts automatically.',
- ],
- ])('preserves Accounts search while %s', async (state, message) => {
- const refetch = vi.fn()
- mocks.accounts.mockReturnValue(
- state === 'loading'
- ? { isPending: true }
- : state === 'error'
- ? { isError: true, error: new Error(message), refetch }
- : {
- data: {
- credentialGroup:
- state === 'missing group' ? null : { ...credentialGroup, options: [] },
- },
- isPending: false,
- }
- )
- await render('google_drive', '?view=accounts&credential-group-people=alex&search=handbook')
-
- expect(container.textContent).toContain(message)
- expect(container.querySelector('input[placeholder="Search people..."]')).toHaveValue('alex')
- expect(container.querySelector('input[placeholder="Search people..."]')).toBeEnabled()
- expect(container.querySelector('input[placeholder="Search sync configurations..."]')).toBeNull()
- expect(mocks.people).not.toHaveBeenCalled()
- if (state === 'error') {
- await click('Try again')
- expect(refetch).toHaveBeenCalledOnce()
- }
-
- const sourcesTab = Array.from(
- container.querySelectorAll('[role="radio"]')
- ).find((item) => item.textContent === 'Advanced')
- expect(sourcesTab).toBeDefined()
- await act(async () => sourcesTab!.click())
- expect(
- container.querySelector('input[placeholder="Search sync configurations..."]')
- ).toHaveValue('handbook')
- await click('Accounts')
- expect(container.querySelector('input[placeholder="Search people..."]')).toHaveValue('alex')
- })
-
it('preserves source navigation and retries connection availability failures', async () => {
mocks.availabilityError = new Error('Connection availability could not be loaded')
await render('google_drive', '?view=sources')
@@ -655,6 +531,19 @@ describe('organization provider management', () => {
expect(mocks.activate).not.toHaveBeenCalled()
})
+ it('explains unavailable integrations instead of showing an empty setup dead end', async () => {
+ mocks.access = { admin: false, members: false }
+ mocks.overview.mockReturnValue({
+ data: { providers: [{ ...provider, connectorType: 'gitlab' }] },
+ })
+ mocks.sources.mockReturnValue({ data: [], isPending: false })
+ await render('gitlab')
+ expect(container.textContent).toContain('Unavailable in this deployment')
+ expect(container.textContent).toContain('GitLab must be configured for this deployment')
+ expect(container.textContent).not.toContain('No GitLab projects added')
+ expect(container.textContent).not.toContain('Add project')
+ })
+
it('explains and retries Slack account lookup failures without hiding its sources', async () => {
const refetch = vi.fn()
mocks.access = { admin: false, members: true }
@@ -677,20 +566,6 @@ describe('organization provider management', () => {
expect(refetch).toHaveBeenCalledOnce()
})
- it('scopes account management and connection requests to the current provider option', async () => {
- await render('google_drive', '?view=accounts')
- expect(mocks.sources).toHaveBeenCalledWith(
- expect.any(Object),
- expect.objectContaining({ enabled: false })
- )
- expect(mocks.people).toHaveBeenCalledWith(
- expect.objectContaining({
- organizationId: 'org-one',
- searchConnection: { optionId: 'google-option', providerName: 'Google Drive' },
- })
- )
- })
-
it.each([
{ type: 'google_drive', access: { admin: true, members: true }, memberParam: false },
{ type: 'gmail', access: { admin: false, members: true }, memberParam: true },
@@ -702,8 +577,7 @@ describe('organization provider management', () => {
data: { providers: [{ ...provider, connectorType: type }] },
})
await render(type)
- await click('Advanced')
- await click('Add sync configuration')
+ await click(memberParam ? 'Set up member accounts' : 'Connect service account')
await vi.waitFor(() => {
expect(mocks.updateUrl).toHaveBeenCalled()
const query = new URLSearchParams(mocks.updateUrl.mock.calls.at(-1)![0].queryString)
@@ -740,7 +614,7 @@ describe('organization provider management', () => {
slackBotCredentialId: 'slack-bot',
configurationStatus: 'needs_update',
},
- ])('offers Slack Accounts recovery for an active option with $name', async (option) => {
+ ])('offers Slack app recovery for an active option with $name', async (option) => {
mocks.access = { admin: false, members: true }
mocks.overview.mockReturnValue({
data: { providers: [{ ...provider, connectorType: 'slack' }] },
@@ -762,24 +636,21 @@ describe('organization provider management', () => {
},
isPending: false,
})
- await render('slack', '?view=accounts&credential-group-people=alex')
+ await render('slack')
expect(mocks.people).not.toHaveBeenCalled()
- expect(container.textContent).toContain('Set up the Slack app to connect accounts.')
- expect(container.querySelector('input[placeholder="Search people..."]')).toHaveValue('alex')
+ expect(container.textContent).toContain('Set up Slack app')
await click('Set up Slack app')
await act(async () => {
await vi.waitFor(() => expect(mocks.updateUrl).toHaveBeenCalled())
})
const query = new URLSearchParams(mocks.updateUrl.mock.calls.at(-1)![0].queryString)
expect(query.get('connectedAccounts')).toBe('slack')
- expect(query.get('view')).toBe('accounts')
- expect(query.get('credential-group-people')).toBe('alex')
expect(query.has('addConnector')).toBe(false)
})
it.each([undefined, 'slack-bot'])(
- 'uses verified configuration for account management with bot credential %s',
+ 'allows source setup with verified Slack configuration and bot credential %s',
async (slackBotCredentialId) => {
mocks.access = { admin: false, members: true }
mocks.overview.mockReturnValue({
@@ -804,11 +675,8 @@ describe('organization provider management', () => {
})
await render('slack', '?view=accounts')
- expect(mocks.people).toHaveBeenCalledWith(
- expect.objectContaining({
- searchConnection: { optionId: 'slack-option', providerName: 'Slack' },
- })
- )
+ expect(container.textContent).toContain('Add channels or DMs')
+ expect(mocks.people).not.toHaveBeenCalled()
expect(container.textContent).not.toContain('Set up the Slack app to connect accounts.')
}
)
diff --git a/apps/sim/app/o/[organizationId]/settings/integrations/providers/[connectorType]/provider-detail.tsx b/apps/sim/app/o/[organizationId]/settings/integrations/providers/[connectorType]/provider-detail.tsx
index cbf18f2f4e0..05b2e177c68 100644
--- a/apps/sim/app/o/[organizationId]/settings/integrations/providers/[connectorType]/provider-detail.tsx
+++ b/apps/sim/app/o/[organizationId]/settings/integrations/providers/[connectorType]/provider-detail.tsx
@@ -1,28 +1,20 @@
'use client'
import { useState } from 'react'
-import { ChipConfirmModal, ChipModalError, ChipSwitch } from '@sim/emcn'
+import { ChipConfirmModal, ChipModalError } from '@sim/emcn'
import { ArrowLeft, Plus } from '@sim/emcn/icons'
import { format } from 'date-fns'
import { useRouter } from 'next/navigation'
import { useQueryState, useQueryStates } from 'nuqs'
import type { SettingsAction } from '@/components/settings/settings-header'
import { SettingsPanel } from '@/components/settings/settings-panel'
-import { findCredentialGroupProviderFromProviderId } from '@/lib/credential-groups/providers'
import { organizationRoutes } from '@/lib/navigation/paths'
-import { getServiceConfigByProviderId, getServiceConfigByServiceId } from '@/lib/oauth'
-import {
- canConnectPersonally,
- canConnectWithDefaults,
- getConnectorAccessAvailability,
-} from '@/lib/sim-search/connectors'
+import { getSearchConnectionLabels } from '@/lib/sim-search/connection-labels'
+import { getConnectorAccessAvailability } from '@/lib/sim-search/connectors'
import { SEARCH_DEBOUNCE_MS } from '@/lib/url-state'
import { useOrganizationContext } from '@/app/o/[organizationId]/providers/organization-provider'
import { organizationSearchStatusLabel } from '@/app/o/[organizationId]/settings/components/integrations/organization-search-status'
-import {
- connectedAccountsParam,
- organizationProviderTabParam,
-} from '@/app/o/[organizationId]/settings/components/integrations/search-params'
+import { connectedAccountsParam } from '@/app/o/[organizationId]/settings/components/integrations/search-params'
import { OrganizationSlackAccountRemoval } from '@/app/o/[organizationId]/settings/components/integrations/slack-account-removal'
import { OrganizationSlackAccountSetup } from '@/app/o/[organizationId]/settings/components/integrations/slack-account-setup'
import { SearchSourcePagination } from '@/app/workspace/[workspaceId]/search/components/search-source-pagination'
@@ -41,12 +33,10 @@ import {
} from '@/app/workspace/[workspaceId]/settings/components/settings-resource-row'
import { useSettingsSearch } from '@/app/workspace/[workspaceId]/settings/components/use-settings-search'
import { CONNECTOR_META_REGISTRY } from '@/connectors/registry'
-import { OrganizationAccountPeople } from '@/ee/credential-groups/components/organization-account-people'
import { useOrganizationSearchOverview, useSearchSources } from '@/hooks/queries/kb/connectors'
import { useOrganizationAccounts } from '@/hooks/queries/organization-accounts'
import { useUpdateSearchIntegration } from '@/hooks/queries/search-integrations'
import { useDebounce } from '@/hooks/use-debounce'
-import { useOrganizationAccountPeopleSearch } from '@/hooks/use-organization-account-people-search'
import { usePermissionConfig } from '@/hooks/use-permission-config'
interface OrganizationProviderDetailProps {
@@ -57,31 +47,21 @@ export function OrganizationProviderDetail({ connectorType }: OrganizationProvid
const { organization, viewer, searchAccess } = useOrganizationContext()
const router = useRouter()
const meta = CONNECTOR_META_REGISTRY[connectorType]
- const automaticSetup = Boolean(meta && canConnectWithDefaults(meta) && searchAccess.memberScoped)
- const [view, setView] = useQueryState(
- organizationProviderTabParam.key,
- organizationProviderTabParam.parser
- )
const [search, setSearch] = useSettingsSearch()
- const [peopleSearch, setPeopleSearch] = useOrganizationAccountPeopleSearch()
const sourceSearch = useDebounce(search.trim(), SEARCH_DEBOUNCE_MS)
const [deactivating, setDeactivating] = useState(false)
const [removingSlackAccounts, setRemovingSlackAccounts] = useState(false)
const scope = { kind: 'organization', organizationId: organization.id } as const
- const personal = Boolean(meta && canConnectPersonally(meta) && searchAccess.memberScoped)
- const showAccounts = view === 'accounts' && personal
const overview = useOrganizationSearchOverview(organization.id, { enabled: viewer.isAdmin })
const sources = useSearchSources(scope, {
connectorType,
search: sourceSearch,
- enabled: viewer.isAdmin && !showAccounts,
+ enabled: viewer.isAdmin,
})
const availability = usePermissionConfig()
const approval = useUpdateSearchIntegration()
const accounts = useOrganizationAccounts(
- viewer.isAdmin && (connectorType === 'slack' || (personal && showAccounts))
- ? organization.id
- : undefined
+ viewer.isAdmin && connectorType === 'slack' ? organization.id : undefined
)
const [, setSetup] = useQueryStates(
{
@@ -103,19 +83,11 @@ export function OrganizationProviderDetail({ connectorType }: OrganizationProvid
router.push(organizationRoutes(organization.id).settingsSection('integrations')),
}
if (!viewer.isAdmin || !meta) return null
- const searchField = showAccounts
- ? { value: peopleSearch, onChange: setPeopleSearch, placeholder: 'Search people...' }
- : {
- value: search,
- onChange: setSearch,
- placeholder: automaticSetup ? 'Search sync configurations...' : 'Search sources...',
- }
- const panel = {
- back,
- title: meta.name,
- description: provider ? organizationSearchStatusLabel(provider) : undefined,
- docsLink: meta.searchDocsUrl,
- search: searchField,
+ const labels = getSearchConnectionLabels(connectorType)
+ const searchField = {
+ value: search,
+ onChange: setSearch,
+ placeholder: labels.searchPlaceholder,
}
const access = getConnectorAccessAvailability(meta, availability.integrationAvailability, {
memberAccessAvailable: searchAccess.memberScoped,
@@ -123,16 +95,22 @@ export function OrganizationProviderDetail({ connectorType }: OrganizationProvid
oauthServiceAvailability: availability.oauthServiceAvailability,
isIntegrationAvailabilityReady: availability.isIntegrationAvailabilityReady,
})
- const service =
- meta.auth.mode === 'oauth'
- ? (getServiceConfigByServiceId(meta.auth.provider) ??
- getServiceConfigByProviderId(meta.auth.provider))
- : undefined
- const credentialProvider = service
- ? findCredentialGroupProviderFromProviderId(service.providerId)
- : undefined
+ const unavailable =
+ availability.isIntegrationAvailabilityReady && !access.admin && !access.members
+ const panel = {
+ back,
+ title: meta.name,
+ description:
+ approved && unavailable
+ ? 'Unavailable in this deployment'
+ : provider
+ ? organizationSearchStatusLabel(provider)
+ : undefined,
+ docsLink: meta.searchDocsUrl,
+ search: searchField,
+ }
const option = accounts.data?.credentialGroup?.options.find(
- (item) => item.provider === credentialProvider && item.status === 'active'
+ (item) => item.provider === 'slack' && item.status === 'active'
)
const group = accounts.data?.credentialGroup
const removalActions: SettingsAction[] =
@@ -141,8 +119,8 @@ export function OrganizationProviderDetail({ connectorType }: OrganizationProvid
group?.options.some((item) => item.provider === 'slack')
? [
{
- text: 'Remove account setup',
- textTone: 'error',
+ id: 'delete',
+ text: 'Remove app setup',
disabled: accounts.isFetching,
onSelect: () => setRemovingSlackAccounts(true),
},
@@ -165,14 +143,13 @@ export function OrganizationProviderDetail({ connectorType }: OrganizationProvid
approval.mutate({ organizationId: organization.id, connectorType, approved: true })
const actions: SettingsAction[] = approved
? [
- ...((access.admin || access.members) && (!automaticSetup || !showAccounts)
+ ...(access.admin || access.members
? [
{
text: needsSlackSetup
? 'Set up Slack app'
- : automaticSetup
- ? 'Add sync configuration'
- : 'Add source',
+ : getSearchConnectionLabels(connectorType, access.admin ? 'admin' : 'members')
+ .add,
icon: Plus,
variant: 'primary' as const,
disabled:
@@ -196,6 +173,7 @@ export function OrganizationProviderDetail({ connectorType }: OrganizationProvid
text: provider ? 'Activate' : 'Add integration',
variant: 'primary',
disabled: pending || (!access.admin && !access.members),
+ tooltip: unavailable ? 'This integration is unavailable in this deployment.' : undefined,
onSelect: activate,
},
]
@@ -247,13 +225,13 @@ export function OrganizationProviderDetail({ connectorType }: OrganizationProvid
{sources.isError && !sources.isFetchNextPageError ? (
void sources.refetch()}
variant='inline'
/>
) : sources.isPending ? (
- Loading sources…
+ Loading connections…
) : (
{sources.data?.map((source) => (
@@ -291,12 +269,12 @@ export function OrganizationProviderDetail({ connectorType }: OrganizationProvid
{!sources.data?.length && !sources.hasNextPage && (
{sourceSearch
- ? 'No matching sources'
- : !approved
- ? 'Activate this integration to set up sources.'
- : automaticSetup
- ? 'No sync configurations yet.'
- : 'No sources yet.'}
+ ? 'No matching connections'
+ : unavailable
+ ? `${meta.name} must be configured for this deployment before you can add a connection.`
+ : !approved
+ ? 'Activate this integration to add a connection.'
+ : labels.empty}
)}
@@ -306,81 +284,7 @@ export function OrganizationProviderDetail({ connectorType }: OrganizationProvid
)
return (
<>
-
- {personal && (
-
- void setView(value)}
- options={
- automaticSetup
- ? [
- { value: 'accounts', label: 'Accounts' },
- { value: 'sources', label: 'Advanced' },
- ]
- : [
- { value: 'sources', label: 'Sources' },
- { value: 'accounts', label: 'Accounts' },
- ]
- }
- />
-
- )}
- {showAccounts ? (
- accounts.isError ? (
-
- void accounts.refetch()}
- variant='inline'
- />
-
- ) : accounts.isPending ? (
-
- Loading accounts…
-
- ) : option && approved && !needsSlackSetup ? (
-
- ) : (
-
- {approval.error && (
-
- {approval.error.message}
-
- )}
- {availability.integrationAvailabilityError && (
- void availability.refetchIntegrationAvailability()}
- variant='inline'
- />
- )}
-
- {approved
- ? needsSlackSetup
- ? 'Set up the Slack app to connect accounts.'
- : automaticSetup
- ? provider && provider.sourceCount > 0
- ? 'No connected member accounts.'
- : 'Members connect their accounts from Integrations. Indexing starts automatically.'
- : 'Add a source to set up account connections.'
- : 'Activate this integration to set up account connections.'}
-
-
- )
- ) : (
- renderSources()
- )}
-
+ {renderSources()}
{
)
})
- it.each(['members', 'admin'] as const)(
- 'links member sources to personal Search connections: %s',
- async (accessMode) => {
- mocks.detail.mockReturnValue({ data: { ...connector, accessMode } })
- await render()
- const link = container.querySelector('a[aria-label="Manage your Search accounts"]')
- if (accessMode === 'members') {
- expect(link).toHaveAttribute('href', '/o/org-one/integrations')
- expect(container.textContent).toContain(
- 'Each person connects from Integrations to sync content they can access.'
- )
- } else {
- expect(link).toBeNull()
- }
- }
- )
it('restores document search and status from the shared URL', async () => {
await render('?search=notes&document-filter=excluded')
expect(mocks.documents).toHaveBeenLastCalledWith(
@@ -239,8 +223,10 @@ describe('organization source detail navigation', () => {
})
await render(searchParams)
- expect(container.textContent).toContain('Some source updates are incomplete')
- expect(container.textContent).toContain('Review the source settings and try syncing again.')
+ expect(container.textContent).toContain('Some connection updates are incomplete')
+ expect(container.textContent).toContain(
+ 'Review the connection settings and try syncing again.'
+ )
expect(container.textContent).not.toContain('Provider denied')
expect(container.textContent).not.toContain('org-private-id')
}
@@ -248,8 +234,10 @@ describe('organization source detail navigation', () => {
it('keeps a healthy active source quiet', async () => {
await render()
- expect(container.textContent).not.toContain('Some source updates are incomplete')
- expect(container.textContent).not.toContain('Review the source settings and try syncing again.')
+ expect(container.textContent).not.toContain('Some connection updates are incomplete')
+ expect(container.textContent).not.toContain(
+ 'Review the connection settings and try syncing again.'
+ )
})
it.each(['', '?view=settings', '?view=history'])(
@@ -293,7 +281,7 @@ describe('organization source detail navigation', () => {
async (status) => {
mocks.detail.mockReturnValue({ data: { ...connector, status, lastSyncError: 'Old failure' } })
await render()
- expect(container.textContent).not.toContain('Some source updates are incomplete')
+ expect(container.textContent).not.toContain('Some connection updates are incomplete')
}
)
@@ -319,7 +307,9 @@ describe('organization source detail navigation', () => {
},
})
await render()
- expect(container.textContent?.includes('Some source updates are incomplete')).toBe(showNotice)
+ expect(container.textContent?.includes('Some connection updates are incomplete')).toBe(
+ showNotice
+ )
expect(container.textContent).not.toContain('private-account-id')
}
)
@@ -351,7 +341,7 @@ describe('organization source detail navigation', () => {
await render()
expect(mocks.actions).not.toHaveBeenCalled()
expect(mocks.documents).not.toHaveBeenCalled()
- expect(container.textContent).toContain('Loading source')
+ expect(container.textContent).toContain('Loading connection')
})
it('does not load protected source data for non-admins', async () => {
mocks.admin = false
@@ -368,7 +358,7 @@ describe('organization source detail navigation', () => {
mocks.index.mockReturnValue({ data: { knowledgeBaseId: null }, isPending: false })
mocks.detail.mockReturnValue({})
await render()
- expect(container.textContent).toContain('This source is no longer available')
+ expect(container.textContent).toContain('This connection is no longer available')
expect(mocks.actions).not.toHaveBeenCalled()
await click('Sources')
expect(mocks.push).toHaveBeenCalledWith('/o/org-one/settings/integrations')
@@ -402,7 +392,7 @@ describe('organization source detail navigation', () => {
const sync = Array.from(container.querySelectorAll('button')).find(
(item) => item.textContent === 'Sync now'
)
- const tabs = container.querySelector('[aria-label="Source views"]')
+ const tabs = container.querySelector('[aria-label="Connection views"]')
expect(sync).toBeTruthy()
expect(tabs).toBeTruthy()
expect(sync!.compareDocumentPosition(tabs!)).toBe(Node.DOCUMENT_POSITION_FOLLOWING)
diff --git a/apps/sim/app/o/[organizationId]/settings/integrations/sources/[connectorId]/source-detail.tsx b/apps/sim/app/o/[organizationId]/settings/integrations/sources/[connectorId]/source-detail.tsx
index 1439c907041..939dfd52563 100644
--- a/apps/sim/app/o/[organizationId]/settings/integrations/sources/[connectorId]/source-detail.tsx
+++ b/apps/sim/app/o/[organizationId]/settings/integrations/sources/[connectorId]/source-detail.tsx
@@ -70,7 +70,7 @@ export function OrganizationSourceDetail({ connectorId }: OrganizationSourceDeta
if (!viewer.isAdmin)
return (
-
+
Only organization admins can manage sources.
@@ -89,17 +89,17 @@ export function OrganizationSourceDetail({ connectorId }: OrganizationSourceDeta
)
if (failedQuery && (!hasCanonicalDetail || accessFailure))
return (
-
+
{isApiClientError(failedQuery.error) && failedQuery.error.status === 404 ? (
- This source is no longer available.
+ This connection is no longer available.
) : (
void failedQuery.refetch()}
- fallback='Could not load source'
+ fallback='Could not load connection'
variant='inline'
/>
)}
@@ -107,9 +107,9 @@ export function OrganizationSourceDetail({ connectorId }: OrganizationSourceDeta
)
if (!index.isPending && !knowledgeBaseId)
return (
-
+
- This source is no longer available.
+ This connection is no longer available.
)
@@ -120,8 +120,8 @@ export function OrganizationSourceDetail({ connectorId }: OrganizationSourceDeta
detail.data.knowledgeBaseId !== knowledgeBaseId
)
return (
-
- Loading source…
+
+ Loading connection…
)
return (
@@ -137,7 +137,7 @@ export function OrganizationSourceDetail({ connectorId }: OrganizationSourceDeta
error={failedQuery.error}
isRetrying={failedQuery.isFetching}
onRetry={() => void failedQuery.refetch()}
- fallback='Could not refresh source'
+ fallback='Could not refresh connection'
variant='inline'
/>
) : undefined
@@ -184,7 +184,7 @@ function SourceDetailContent({
const meta = CONNECTOR_META_REGISTRY[connector.connectorType]
const title = meta
? describeSearchSource(meta, connector.sourceConfig) || meta.name
- : 'Search source'
+ : 'Connection'
const { effectiveStatus, lastSyncError } = getConnectorSyncState(connector)
const status =
effectiveStatus === 'paused'
@@ -209,7 +209,7 @@ function SourceDetailContent({
return (
)}
- {connector.accessMode === 'members' && (
-
- )}
>
)
if (view === 'settings')
@@ -279,8 +270,8 @@ function SourceDetailContent({
{effectiveStatus === 'active' && lastSyncError && (
)}
)
diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/search-integration-connection.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/search-integration-connection.tsx
index e1df655f9ce..5428ce8551d 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/search-integration-connection.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/search-integration-connection.tsx
@@ -53,7 +53,8 @@ function SearchIntegrationConnectionControl({
})
const name = connector?.meta.name ?? target.provider
const action = target.credentialId ? 'Reconnect' : 'Connect'
- const label = connection.connected
+ const connected = connection.connected
+ const label = connected
? `Connected ${name}`
: connection.isLoading
? `Checking ${name} connections…`
@@ -76,18 +77,14 @@ function SearchIntegrationConnectionControl({
!connector ||
connection.isLoading ||
connection.isStarting ||
- connection.connected ||
+ connected ||
(!connection.available && !connection.pending)
}
onClick={handleConnect}
leading={
connector &&
}
- trailing={
- connection.connected ? (
-
- ) : undefined
- }
+ trailing={connected ? : undefined}
/>
{connection.pending && Cancel }
{connection.error && (
@@ -100,6 +97,8 @@ function SearchIntegrationConnectionControl({
)}
{setupOpen && connector && (
setSetupOpen(false)}
isPending={connection.isStarting}
diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/search-sources/atlassian-source-setup-modal.test.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/search-sources/atlassian-source-setup-modal.test.tsx
new file mode 100644
index 00000000000..44e7fe18616
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/home/components/search-sources/atlassian-source-setup-modal.test.tsx
@@ -0,0 +1,172 @@
+/** @vitest-environment jsdom */
+import { act } from 'react'
+import { createRoot, type Root } from 'react-dom/client'
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import type { PersonalSourceSetupAccounts } from '@/lib/api/contracts/knowledge/personal-source-setup'
+import type { ConnectorConfigField } from '@/connectors/types'
+
+const m = vi.hoisted(() => ({
+ connect: vi.fn(),
+ authorize: vi.fn(),
+ close: vi.fn(),
+ complete: vi.fn(),
+ accounts: [] as PersonalSourceSetupAccounts['accounts'],
+ pending: false,
+}))
+vi.mock('@/hooks/use-personal-source-account', () => ({
+ usePersonalSourceAccount: () => ({
+ accounts: {
+ data: { accounts: m.accounts, completedCredentialId: null },
+ isPending: false,
+ isError: false,
+ },
+ connect: m.authorize,
+ cancel: vi.fn(),
+ pending: m.pending,
+ }),
+}))
+vi.mock('@/hooks/queries/personal-source-setup', () => ({
+ useConnectPersonalSourceSetup: () => ({ mutateAsync: m.connect, isPending: false }),
+}))
+vi.mock(
+ '@/app/workspace/[workspaceId]/knowledge/[id]/components/connector-selector-field/connector-selector-field',
+ () => ({
+ ConnectorSelectorField: (props: {
+ field: ConnectorConfigField
+ value: string[]
+ onChange: (value: string[]) => void
+ credentialId: string
+ selectorSurface: { kind: string }
+ }) => (
+ props.onChange(['ENG', 'SUPPORT'])}
+ >
+ {props.value.join(',') || props.field.placeholder}
+
+ ),
+ })
+)
+
+import { SEARCH_CONNECTORS } from '@/lib/sim-search/connectors'
+import { AtlassianSourceSetupModal } from '@/app/workspace/[workspaceId]/home/components/search-sources/atlassian-source-setup-modal'
+
+let root: Root
+let container: HTMLDivElement
+function button(label: string) {
+ const found = Array.from(document.querySelectorAll('button')).find(
+ (node) => node.textContent?.trim() === label
+ )
+ if (!found) throw new Error(`Missing button: ${label}`)
+ return found
+}
+function input(placeholder: string) {
+ const found = document.querySelector(`input[placeholder="${placeholder}"]`)
+ if (!found) throw new Error(`Missing input: ${placeholder}`)
+ return found
+}
+function fill(field: HTMLInputElement, value: string) {
+ act(() => {
+ Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')!.set!.call(field, value)
+ field.dispatchEvent(new Event('input', { bubbles: true }))
+ })
+}
+function render(type: 'jira' | 'confluence' = 'jira') {
+ const connector = SEARCH_CONNECTORS.find((item) => item.type === type)!
+ act(() =>
+ root.render(
+
+ )
+ )
+}
+beforeEach(() => {
+ vi.clearAllMocks()
+ m.accounts = []
+ m.pending = false
+ m.connect.mockResolvedValue({
+ kind: 'connected',
+ connectorId: 'source-1',
+ knowledgeBaseId: 'kb-1',
+ })
+ ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
+ vi.stubGlobal(
+ 'ResizeObserver',
+ class {
+ observe() {}
+ unobserve() {}
+ disconnect() {}
+ }
+ )
+ container = document.createElement('div')
+ document.body.appendChild(container)
+ root = createRoot(container)
+})
+afterEach(() => {
+ act(() => root.unmount())
+ container.remove()
+ vi.unstubAllGlobals()
+})
+describe('Atlassian personal source setup', () => {
+ it('starts with account authorization and hides content fields until an account exists', () => {
+ render()
+ expect(document.querySelector('[data-testid="picker"]')).toBeNull()
+ expect(button('Connect & Sync')).toBeDisabled()
+ act(() => button('Connect account').click())
+ expect(m.authorize).toHaveBeenCalledOnce()
+ expect(m.connect).not.toHaveBeenCalled()
+ })
+ it.each(['jira', 'confluence'] as const)(
+ 'reuses an account and preserves selected %s keys through manual mode and submission',
+ async (type) => {
+ m.accounts = [
+ { id: 'my-account', name: 'My account', provider: type, type: 'managed_oauth', scopes: [] },
+ ]
+ render(type)
+ fill(input('yoursite.atlassian.net'), 'team.atlassian.net')
+ const picker = document.querySelector('[data-testid="picker"]')!
+ expect(picker.dataset.credential).toBe('my-account')
+ expect(picker.dataset.surface).toBe('personal-search-setup')
+ act(() => picker.click())
+ act(() => button('Enter keys manually').click())
+ const manual = document.querySelector(
+ `input[aria-label="${type === 'jira' ? 'Project Keys' : 'Space Keys'}"]`
+ )!
+ expect(manual.value).toBe('ENG, SUPPORT')
+ fill(manual, 'ENG, PRODUCT')
+ act(() => button(`Choose ${type === 'jira' ? 'projects' : 'spaces'} from list`).click())
+ expect(document.querySelector('[data-testid="picker"]')?.textContent).toBe('ENG,PRODUCT')
+ await act(async () => button('Connect & Sync').click())
+ expect(m.connect).toHaveBeenCalledWith({
+ action: 'connect',
+ organizationId: 'org-1',
+ connectorType: type,
+ credentialId: 'my-account',
+ domain: 'team.atlassian.net',
+ keys: ['ENG', 'PRODUCT'],
+ })
+ expect(m.authorize).not.toHaveBeenCalled()
+ expect(m.complete).toHaveBeenCalledWith({
+ connectorId: 'source-1',
+ credentialId: 'my-account',
+ })
+ }
+ )
+ it('clears the selection when the site changes', () => {
+ m.accounts = [
+ { id: 'my-account', name: 'My account', provider: 'jira', type: 'managed_oauth', scopes: [] },
+ ]
+ render()
+ fill(input('yoursite.atlassian.net'), 'team.atlassian.net')
+ act(() => document.querySelector('[data-testid="picker"]')!.click())
+ fill(input('yoursite.atlassian.net'), 'other.atlassian.net')
+ expect(button('Connect & Sync')).toBeDisabled()
+ })
+})
diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/search-sources/atlassian-source-setup-modal.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/search-sources/atlassian-source-setup-modal.tsx
new file mode 100644
index 00000000000..4b60deaa7cb
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/home/components/search-sources/atlassian-source-setup-modal.tsx
@@ -0,0 +1,241 @@
+'use client'
+
+import { useState } from 'react'
+import {
+ Chip,
+ ChipCombobox,
+ ChipInput,
+ ChipModal,
+ ChipModalBody,
+ ChipModalField,
+ ChipModalFooter,
+ ChipModalHeader,
+ toast,
+} from '@sim/emcn'
+import { getErrorMessage } from '@sim/utils/errors'
+import type { PersonalSourceSetupQuery } from '@/lib/api/contracts/knowledge/personal-source-setup'
+import type { SearchConnector } from '@/lib/sim-search/connectors'
+import { MAX_PERSONAL_SOURCE_SETUP_KEYS } from '@/lib/sim-search/personal-source-setup'
+import { ConnectorSelectorField } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/connector-selector-field/connector-selector-field'
+import { useConnectorConfigFields } from '@/app/workspace/[workspaceId]/knowledge/[id]/hooks/use-connector-config-fields'
+import { useConnectPersonalSourceSetup } from '@/hooks/queries/personal-source-setup'
+import { usePersonalSourceAccount } from '@/hooks/use-personal-source-account'
+
+interface AtlassianSourceSetupModalProps {
+ organizationId: string
+ connector: SearchConnector
+ connectorType: PersonalSourceSetupQuery['connectorType']
+ onClose: () => void
+ onConnected?: (connection: { connectorId: string; credentialId: string }) => void
+}
+
+export function AtlassianSourceSetupModal({
+ organizationId,
+ connector,
+ connectorType,
+ onClose,
+ onConnected,
+}: AtlassianSourceSetupModalProps) {
+ const account = usePersonalSourceAccount({
+ organizationId,
+ connectorType,
+ onConnected: (id) => {
+ setSelectedAccount(id)
+ config.setSourceConfig((previous) => ({ domain: previous.domain ?? '' }))
+ },
+ })
+ const { mutateAsync: connect, isPending } = useConnectPersonalSourceSetup()
+ const config = useConnectorConfigFields({
+ connectorConfig: connector.meta,
+ accessMode: 'members',
+ })
+ const [selectedAccount, setSelectedAccount] = useState()
+ const accounts = account.accounts.data?.accounts ?? []
+ const requestedAccount =
+ selectedAccount ??
+ account.accounts.data?.completedCredentialId ??
+ (accounts.length === 1 ? accounts[0].id : undefined)
+ const credentialId = accounts.find((item) => item.id === requestedAccount)?.id ?? null
+ const canonicalId = connectorType === 'jira' ? 'projectKey' : 'spaceKey'
+ const picker = connector.meta.configFields.find(
+ (field) => field.canonicalParamId === canonicalId && field.type === 'selector'
+ )!
+ const manual = connector.meta.configFields.find((field) => field.id === canonicalId)!
+ const advanced = config.canonicalModes[canonicalId] === 'advanced'
+ const domain = typeof config.sourceConfig.domain === 'string' ? config.sourceConfig.domain : ''
+ const resolved = config.resolveSourceConfig()[canonicalId]
+ const manualValue = config.sourceConfig[canonicalId]
+ const keys = Array.isArray(resolved)
+ ? resolved
+ .filter((key): key is string => typeof key === 'string' && Boolean(key.trim()))
+ .map((key) => key.trim())
+ : []
+ const pending = isPending || account.pending
+ const close = () => {
+ if (!isPending) onClose()
+ }
+ const chooseAccount = (id: string) => {
+ if (id === credentialId) return
+ setSelectedAccount(id)
+ config.setSourceConfig({ domain })
+ }
+ const addAccount = () => {
+ void account.connect()
+ }
+ const submit = async () => {
+ if (!credentialId || !domain.trim() || !keys.length || pending) return
+ if (keys.length > MAX_PERSONAL_SOURCE_SETUP_KEYS) {
+ toast.error('Choose no more than 1,000 projects or spaces per source.')
+ return
+ }
+ try {
+ const result = await connect({
+ action: 'connect',
+ organizationId,
+ connectorType,
+ credentialId,
+ domain: domain.trim(),
+ keys,
+ })
+ onConnected?.({ connectorId: result.connectorId, credentialId })
+ onClose()
+ } catch (error) {
+ toast.error(getErrorMessage(error, 'Could not connect the source'))
+ }
+ }
+
+ return (
+ {
+ if (!open) close()
+ }}
+ srTitle={`Connect ${connector.meta.name}`}
+ >
+ Connect {connector.meta.name}
+
+
+ {(aria) => (
+ <>
+ {accounts.length > 0 && (
+ ({ value: item.id, label: item.name }))}
+ placeholder='Select your account'
+ disabled={pending}
+ />
+ )}
+
+
+ {account.pending
+ ? 'Waiting for authorization…'
+ : accounts.length
+ ? 'Connect another account'
+ : 'Connect account'}
+
+ {account.pending && Cancel }
+
+ {account.accounts.isPending && (
+ Loading accounts…
+ )}
+ {account.accounts.isError && (
+ void account.accounts.refetch()}>Retry loading accounts
+ )}
+ >
+ )}
+
+ {credentialId && !account.pending && (
+ <>
+ config.handleFieldChange('domain', value)}
+ placeholder='yoursite.atlassian.net'
+ autoComplete='off'
+ required
+ disabled={isPending}
+ />
+
+ {(aria) => (
+ <>
+ {advanced ? (
+
+ config.handleFieldChange(canonicalId, event.target.value)
+ }
+ placeholder={manual.placeholder}
+ disabled={isPending}
+ />
+ ) : picker.selectorKey ? (
+
+ config.handleFieldChange(picker.id, value, labels)
+ }
+ credentialId={credentialId}
+ sourceConfig={config.sourceConfig}
+ configFields={connector.meta.configFields}
+ canonicalModes={config.canonicalModes}
+ selectedLabels={config.selectionLabels[canonicalId]}
+ disabled={isPending}
+ />
+ ) : null}
+ config.toggleCanonicalMode(canonicalId)}
+ disabled={isPending}
+ >
+ {advanced
+ ? `Choose ${connectorType === 'jira' ? 'projects' : 'spaces'} from list`
+ : 'Enter keys manually'}
+
+ >
+ )}
+
+ >
+ )}
+
+
+ window.open(connector.meta.searchDocsUrl, '_blank', 'noopener,noreferrer'),
+ },
+ ]
+ : undefined
+ }
+ primaryAction={{
+ label: isPending ? 'Connecting…' : 'Connect & Sync',
+ onClick: () => void submit(),
+ disabled: !credentialId || !domain.trim() || !keys.length || pending,
+ }}
+ />
+
+ )
+}
diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/search-sources/source-setup-modal.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/search-sources/source-setup-modal.tsx
index 4b73d5b989c..eb6f15699a4 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/components/search-sources/source-setup-modal.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/home/components/search-sources/source-setup-modal.tsx
@@ -9,8 +9,11 @@ import {
ChipModalHeader,
} from '@sim/emcn'
import type { SearchConnector } from '@/lib/sim-search/connectors'
+import { AtlassianSourceSetupModal } from '@/app/workspace/[workspaceId]/home/components/search-sources/atlassian-source-setup-modal'
interface SourceSetupModalProps {
+ organizationId?: string
+ onConnected?: (connection: { connectorId: string; credentialId: string }) => void
connector: SearchConnector
onClose: () => void
isPending?: boolean
@@ -23,7 +26,26 @@ interface SourceSetupModalProps {
* The few fields a source needs before its first connect, such as a site and
* a space. Everyone after the first person clicks straight through.
*/
-export function SourceSetupModal({
+export function SourceSetupModal(props: SourceSetupModalProps) {
+ if (
+ props.organizationId &&
+ (props.connector.type === 'jira' || props.connector.type === 'confluence')
+ ) {
+ return (
+
+ )
+ }
+ return
+}
+
+function ManualSourceSetupModal({
connector,
onClose,
onConnect,
diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.test.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.test.tsx
index 2dbccfee687..daee1e5c632 100644
--- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.test.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.test.tsx
@@ -13,6 +13,7 @@ import type { ConfigFieldMap } from '@/app/workspace/[workspaceId]/knowledge/[id
const mocks = vi.hoisted(() => ({
create: vi.fn(),
accountsQuery: vi.fn(),
+ oauthQuery: vi.fn(),
configFields: vi.fn(),
resolveSourceConfig: vi.fn((): Record => ({})),
sourceConfig: {} as ConfigFieldMap,
@@ -130,14 +131,17 @@ vi.mock('@/hooks/queries/source-accounts', () => ({
},
}))
vi.mock('@/hooks/queries/oauth/oauth-credentials', () => ({
- useOAuthCredentials: () => ({
- data: mocks.credentials,
- isLoading: mocks.credentialsState === 'loading',
- isSuccess: mocks.credentialsState === 'ready',
- isFetching: mocks.credentialsState === 'loading',
- error: mocks.credentialsState === 'error' ? new Error('Could not load accounts') : null,
- refetch: mocks.refetchCredentials,
- }),
+ useOAuthCredentials: (...args: unknown[]) => {
+ mocks.oauthQuery(...args)
+ return {
+ data: mocks.credentials,
+ isLoading: mocks.credentialsState === 'loading',
+ isSuccess: mocks.credentialsState === 'ready',
+ isFetching: mocks.credentialsState === 'loading',
+ error: mocks.credentialsState === 'error' ? new Error('Could not load accounts') : null,
+ refetch: mocks.refetchCredentials,
+ }
+ },
}))
vi.mock('@/hooks/use-oauth-return', () => ({ useOAuthReturnForKBConnectors: vi.fn() }))
vi.mock('@/hooks/use-credential-refresh-triggers', () => ({
@@ -158,6 +162,7 @@ vi.mock(
{
mocks.credentials = [
+ ...mocks.credentials,
{ id: 'new-service-account', name: 'New service account', type: 'service_account' },
]
props.onCreated('new-service-account')
@@ -413,6 +418,58 @@ describe('Slack member setup readiness', () => {
})
describe('Search methods requiring member identity', () => {
+ it.each(['browsing', 'content'] as const)(
+ 'assigns a new service account to the %s field without changing the other credential',
+ async (field) => {
+ mocks.serviceAccountTarget = {
+ serviceAccountProviderId: 'google-service-account',
+ serviceName: 'Google',
+ serviceIcon: googleDriveConnectorMeta.icon,
+ label: 'Add service account',
+ hidden: false,
+ }
+ await render({
+ initialConnectorType: 'google_drive',
+ initialAccessMode: 'members',
+ scope: { kind: 'organization', organizationId: 'org-1' },
+ })
+ await act(async () => button('More options').click())
+ await act(async () => combobox('Source account').click())
+ const sourceOption = Array.from(
+ document.querySelectorAll('[role="option"]')
+ ).find((option) => option.textContent?.trim() === 'Source account')
+ expect(sourceOption).toBeDefined()
+ await act(async () =>
+ sourceOption?.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }))
+ )
+ await act(async () =>
+ combobox(field === 'content' ? 'Connected members' : 'Source account').click()
+ )
+ const serviceOption = Array.from(
+ document.querySelectorAll('[role="option"]')
+ ).find((option) => option.textContent?.trim() === 'Add service account')
+ expect(serviceOption).toBeDefined()
+ await act(async () =>
+ serviceOption?.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }))
+ )
+ await act(async () => button('Finish service account setup').click())
+
+ expect(combobox('New service account')).toBeDefined()
+ expect(configFieldsProps().credentialId).toBe(
+ field === 'content' ? 'credential-1' : 'new-service-account'
+ )
+ expect(combobox(field === 'content' ? 'Source account' : 'Connected members')).toBeDefined()
+ await act(async () => button('Set up member accounts').click())
+ expect(mocks.create).toHaveBeenCalledWith(
+ expect.objectContaining({
+ accessMode: 'members',
+ credentialId: field === 'content' ? 'new-service-account' : undefined,
+ }),
+ expect.any(Object)
+ )
+ }
+ )
+
it('selects a GitHub installation for content while preserving member access', async () => {
mocks.resolveSourceConfig.mockReturnValue({ repository: 'acme/docs' })
await render({
@@ -432,7 +489,7 @@ describe('Search methods requiring member identity', () => {
)
await act(async () => button('Use GitHub installation').click())
expect(combobox('GitHub App: acme')).toBeDefined()
- await act(async () => button('Add source').click())
+ await act(async () => button('Add repository').click())
expect(mocks.create).toHaveBeenCalledWith(
expect.objectContaining({
credentialId: 'github-app-credential',
@@ -467,7 +524,7 @@ describe('Search methods requiring member identity', () => {
expect(document.body.textContent).not.toContain('Sync using')
expect(document.querySelector('button[aria-label="Choose another source"]')).toBeNull()
await act(async () =>
- button(accessMode === 'admin' ? 'Connect & Sync' : 'Add source').click()
+ button(accessMode === 'admin' ? 'Connect & Sync' : 'Add Confluence site').click()
)
expect(mocks.create).toHaveBeenCalledWith(
expect.objectContaining({ connectorType: 'confluence', accessMode }),
@@ -497,7 +554,7 @@ describe('Search methods requiring member identity', () => {
})
expect(document.body.textContent).not.toContain('Sync using')
expect(document.body.textContent).not.toContain('Choose another source')
- await act(async () => button('Add source').click())
+ await act(async () => button('Add Confluence site').click())
expect(mocks.create).toHaveBeenCalledWith(
expect.objectContaining({
connectorType: 'confluence',
@@ -809,6 +866,32 @@ describe('Search setup options', () => {
})
describe('Account connection dropdown', () => {
+ it.each(['jira', 'confluence'])(
+ 'reuses the caller’s managed %s account for browsing member sources',
+ async (connectorType) => {
+ mocks.credentials = [
+ { id: 'managed-account', name: 'My Search account', type: 'managed_oauth' },
+ ]
+ await render({
+ initialConnectorType: connectorType,
+ lockedAccessMode: 'members',
+ scope: { kind: 'organization', organizationId: 'org-1' },
+ })
+ expect(mocks.oauthQuery).toHaveBeenLastCalledWith(
+ expect.any(String),
+ expect.objectContaining({ purpose: 'browsing', organizationId: 'org-1' })
+ )
+ await act(async () => combobox('My Search account').click())
+ const option = Array.from(document.querySelectorAll('[role="option"]')).find(
+ (node) => node.textContent?.trim() === 'My Search account'
+ )
+ expect(option).toBeDefined()
+ await act(async () => option!.dispatchEvent(new MouseEvent('mousedown', { bubbles: true })))
+ expect(mocks.configFields).toHaveBeenLastCalledWith(
+ expect.objectContaining({ credentialId: 'managed-account' })
+ )
+ }
+ )
it.each(['google_drive', 'gmail', 'google_calendar'])(
'opens only service-account creation for a central %s source and submits that credential',
async (connectorType) => {
@@ -858,7 +941,10 @@ describe('Account connection dropdown', () => {
)
it('only offers service accounts when creating a central Confluence source', async () => {
- mocks.credentials = [{ id: 'personal-account', name: 'Personal Confluence', type: 'oauth' }]
+ mocks.credentials = [
+ { id: 'personal-account', name: 'Personal Confluence', type: 'oauth' },
+ { id: 'managed-account', name: 'My Search account', type: 'managed_oauth' },
+ ]
mocks.serviceAccountTarget = {
serviceAccountProviderId: 'atlassian-service-account',
serviceName: 'Atlassian',
@@ -958,7 +1044,7 @@ describe('Account connection dropdown', () => {
})
expect(document.body.textContent).toContain('Account for browsing')
- expect(button('Add source')).toBeEnabled()
+ expect(button('Add Confluence site')).toBeEnabled()
await act(async () => combobox('Select Confluence account').click())
const options = Array.from(document.querySelectorAll('[role="option"]'))
expect(options.map((option) => option.textContent?.trim())).toEqual([
@@ -996,7 +1082,7 @@ describe('Account connection dropdown', () => {
await render({ initialConnectorType: 'confluence', initialAccessMode: 'admin' })
expect(document.body.textContent).toContain('Could not load accounts')
- expect(document.body.textContent).not.toContain('Connect Confluence')
+ expect(document.body.textContent).not.toContain('Connect Confluence account')
expect(button('Connect & Sync')).toBeDisabled()
await act(async () => button('Try again').click())
expect(mocks.refetchCredentials).toHaveBeenCalledOnce()
diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.tsx
index 846081bd5bf..12162fd88c7 100644
--- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.tsx
@@ -27,6 +27,7 @@ import {
getServiceAccountProviderForProviderId,
type OAuthProvider,
} from '@/lib/oauth'
+import { getSearchConnectionLabels } from '@/lib/sim-search/connection-labels'
import { getConnectorAccessAvailability } from '@/lib/sim-search/connectors'
import { SIM_SEARCH_SYNC_INTERVAL_MINUTES } from '@/lib/sim-search/constants'
import { ConnectOAuthModal } from '@/app/workspace/[workspaceId]/components/connect-oauth-modal'
@@ -84,6 +85,7 @@ interface AddConnectorModalProps {
isSearchIndex?: boolean
initialConnectorType?: string | null
initialAccessMode?: ConnectorAccessSelection['accessMode']
+ lockConnectorType?: boolean
/** The entry point has already chosen how this source connects. */
lockedAccessMode?: 'members' | 'admin'
initialSyncIntervalMinutes?: number
@@ -101,6 +103,7 @@ export function AddConnectorModal({
isSearchIndex = false,
initialConnectorType,
initialAccessMode = 'workspace',
+ lockConnectorType = false,
lockedAccessMode,
initialSyncIntervalMinutes = 1440,
onCreated,
@@ -133,7 +136,7 @@ export function AddConnectorModal({
const [access, setAccess] = useState(() => ({
accessMode:
lockedAccessMode ??
- draft?.accessMode ??
+ (lockConnectorType ? initialAccessMode : draft?.accessMode) ??
(isSearchIndex && initialAccessMode === 'workspace'
? initialType && CONNECTOR_META_REGISTRY[initialType]?.auth.mode === 'apiKey'
? 'admin'
@@ -146,7 +149,9 @@ export function AddConnectorModal({
const [showMetadata, setShowMetadata] = useState(false)
const [error, setError] = useState(null)
const [showOAuthModal, setShowOAuthModal] = useState(false)
- const [showServiceAccountModal, setShowServiceAccountModal] = useState(false)
+ const [serviceAccountField, setServiceAccountField] = useState<'browsing' | 'content' | null>(
+ null
+ )
const [showGitHubInstallationModal, setShowGitHubInstallationModal] = useState(false)
const [apiKeyValue, setApiKeyValue] = useState('')
@@ -172,6 +177,11 @@ export function AddConnectorModal({
},
]
: undefined
+ const searchLabels =
+ isSearchIndex && selectedType
+ ? getSearchConnectionLabels(selectedType, access.accessMode)
+ : undefined
+ const modalTitle = searchLabels?.title ?? `Configure ${connectorConfig?.name}`
const isMembersMode = access.accessMode === 'members'
const apiKeyConfig = connectorConfig ? getConnectorApiKeyConfig(connectorConfig.auth) : undefined
const isApiKeyMode =
@@ -243,6 +253,8 @@ export function AddConnectorModal({
!serviceAccountTarget.hidden &&
(deploymentState === 'ready' || deploymentState === 'limited')
+ const browsePersonalAccounts =
+ isMembersMode && (selectedType === 'jira' || selectedType === 'confluence')
const {
data: rawCredentials = [],
isLoading: credentialsLoading,
@@ -251,6 +263,7 @@ export function AddConnectorModal({
refetch: refetchCredentials,
} = useOAuthCredentials(connectorProviderId ?? undefined, {
enabled: Boolean(connectorConfig) && !isApiKeyMode,
+ purpose: browsePersonalAccounts ? 'browsing' : undefined,
...owner,
})
@@ -258,6 +271,7 @@ export function AddConnectorModal({
const credentials = rawCredentials.filter(
(credential) =>
+ (browsePersonalAccounts && credential.type === 'managed_oauth') ||
!connectorConfig ||
isConnectorCredentialTypeAllowed(connectorConfig.auth, access.accessMode, credential.type)
)
@@ -387,7 +401,7 @@ export function AddConnectorModal({
value: '__service_account__',
label: serviceAccountTarget.label,
icon: Plus,
- onSelect: () => setShowServiceAccountModal(true),
+ onSelect: () => setServiceAccountField('content'),
},
]
: []),
@@ -547,14 +561,14 @@ export function AddConnectorModal({
closeSetup(false)}>
{step === 'configure' ? (
- {!lockedAccessMode && (
+ {!lockConnectorType && !lockedAccessMode && (
)}
- {`Configure ${connectorConfig?.name}`}
+ {modalTitle}
) : (
- 'Connect Source'
+ 'Add source'
)}
@@ -679,11 +693,9 @@ export function AddConnectorModal({
title={
isMembersMode
? 'Account for browsing'
- : isSearchIndex
- ? 'Indexing account'
- : canConnectOAuth
- ? 'Account'
- : 'Service account'
+ : canConnectOAuth
+ ? 'Account'
+ : 'Service account'
}
hint={
isSearchIndex && isMembersMode
@@ -728,7 +740,7 @@ export function AddConnectorModal({
label: serviceAccountTarget.label,
value: '__service_account__',
icon: Plus,
- onSelect: () => setShowServiceAccountModal(true),
+ onSelect: () => setServiceAccountField('browsing'),
},
]
: []),
@@ -890,7 +902,7 @@ export function AddConnectorModal({
: 'Connecting…'
: isMembersMode
? scope.kind === 'organization'
- ? 'Add source'
+ ? (searchLabels?.add ?? 'Add connection')
: 'Create & Invite'
: 'Connect & Sync',
onClick: handleSubmit,
@@ -899,10 +911,12 @@ export function AddConnectorModal({
/>
))}
- {showServiceAccountModal && canConnectServiceAccount && (
+ {serviceAccountField && canConnectServiceAccount && (
{
+ if (!open) setServiceAccountField(null)
+ }}
{...owner}
serviceAccountProviderId={serviceAccountTarget.serviceAccountProviderId}
serviceName={serviceAccountTarget.serviceName}
@@ -913,7 +927,9 @@ export function AddConnectorModal({
? `${docsUrl}#using-a-service-account`
: undefined
}
- onCreated={setSelectedCredentialId}
+ onCreated={
+ serviceAccountField === 'content' ? setContentCredentialId : setSelectedCredentialId
+ }
/>
)}
{showGitHubInstallationModal &&
@@ -951,7 +967,7 @@ export function AddConnectorModal({
{...owner}
knowledgeBaseId={knowledgeBaseId}
connectorType={selectedType ?? undefined}
- sourceAccess={lockedAccessMode === 'members' ? 'members' : undefined}
+ sourceAccess={access.accessMode === 'members' ? 'members' : undefined}
/>
)}
>
diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-access-field/connector-access-field.test.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-access-field/connector-access-field.test.tsx
index 3c23dda2671..8246cb9404d 100644
--- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-access-field/connector-access-field.test.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-access-field/connector-access-field.test.tsx
@@ -124,7 +124,7 @@ describe('connection method selection', () => {
)
expect(dropdown).toBeDisabled()
expect(dropdown).toHaveTextContent(label)
- expect(container.textContent).toContain('Create a new source to change the sync method.')
+ expect(container.textContent).toContain('Add a new connection to change the sync method.')
expect(container.querySelector('[role="radiogroup"]')).toBeNull()
await act(async () => dropdown!.click())
expect(document.querySelector('[role="menu"]')).toBeNull()
diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-access-field/connector-access-field.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-access-field/connector-access-field.tsx
index 1f355ac085e..3496cde7065 100644
--- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-access-field/connector-access-field.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-access-field/connector-access-field.tsx
@@ -152,7 +152,7 @@ export function ConnectorAccessField({
canAdmin && isAvailabilityReady && !currentMode?.allowed
? `This connection method is not available in this ${scope.kind}.`
: lockAccessMode
- ? 'Create a new source to change the sync method.'
+ ? 'Add a new connection to change the sync method.'
: value.accessMode === 'workspace'
? 'Everyone in this workspace can search these documents.'
: undefined
diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-selector-field/connector-selector-field.queries.test.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-selector-field/connector-selector-field.queries.test.tsx
index d4bb2cd6abf..9958d302243 100644
--- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-selector-field/connector-selector-field.queries.test.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-selector-field/connector-selector-field.queries.test.tsx
@@ -7,10 +7,11 @@ import type { SelectorRequest, SelectorResult } from '@/lib/selectors/types'
import type { ConnectorConfigField } from '@/connectors/types'
interface ComboboxProps {
- options: { value: string; label: string }[]
+ options: { value: string; label: string; disabled?: boolean; onSelect?: () => void }[]
isLoading: boolean
onChange: (value: string) => void
onMultiSelectChange?: (value: string[]) => void
+ onSearchChange?: (value: string) => void
}
const mocks = vi.hoisted(() => ({
@@ -18,7 +19,9 @@ const mocks = vi.hoisted(() => ({
combobox: vi.fn((_props: ComboboxProps) => null),
}))
-vi.mock('@sim/emcn', () => ({ ChipCombobox: mocks.combobox }))
+vi.mock('@sim/emcn', () => ({
+ ChipCombobox: mocks.combobox,
+}))
vi.mock('next/navigation', () => ({ useParams: () => ({ workspaceId: 'workspace-1' }) }))
vi.mock('@/hooks/use-debounce', () => ({ useDebounce: (value: string) => value }))
vi.mock('@/lib/selectors/client/execute-selector', () => ({
@@ -161,3 +164,171 @@ it('keeps the loaded list visible while resolving a saved selection on another p
client.clear()
}
})
+
+async function renderBulkSelector() {
+ const client = new QueryClient({ defaultOptions: { queries: { retry: false } } })
+ const container = document.createElement('div')
+ document.body.appendChild(container)
+ const root = createRoot(container)
+ const change = vi.fn()
+ const rerender = (sourceConfig = { domain: 'example.atlassian.net' }) =>
+ act(async () =>
+ root.render(
+
+
+
+ )
+ )
+ await rerender()
+ const all = () => {
+ const option = mocks.combobox.mock.lastCall![0].options.find((item) => item.label === 'All')
+ if (!option) throw new Error('Missing All option')
+ return option
+ }
+ await act(async () => vi.waitFor(() => expect(all().disabled).toBe(false), { interval: 1 }))
+ return {
+ container,
+ change,
+ all,
+ rerender,
+ dispose: async () => {
+ await act(async () => root.unmount())
+ client.clear()
+ container.remove()
+ },
+ }
+}
+
+it('selects every complete page from All without an external toolbar', async () => {
+ mocks.execute.mockImplementation(async ({ request }: { request: SelectorRequest }) =>
+ request.kind === 'detail'
+ ? { kind: 'detail', item: null }
+ : request.cursor
+ ? { kind: 'list', items: [options[1], options[0]] }
+ : { kind: 'list', items: [options[0]], nextCursor: 'next' }
+ )
+ const view = await renderBulkSelector()
+ try {
+ expect(view.container.textContent).not.toContain('Select all')
+ await act(async () => view.all().onSelect?.())
+ await act(async () =>
+ vi.waitFor(() => expect(view.change).toHaveBeenCalledWith(['ENG', 'OPS'], options), {
+ interval: 1,
+ })
+ )
+ expect(mocks.execute.mock.calls.filter(([args]) => args.request.kind === 'list')).toHaveLength(
+ 2
+ )
+ await act(async () => mocks.combobox.mock.lastCall![0].onMultiSelectChange?.([]))
+ expect(view.change).toHaveBeenLastCalledWith([], [])
+ } finally {
+ await view.dispose()
+ }
+})
+
+it.each(['partial', 'error'] as const)('preserves the selection on %s results', async (failure) => {
+ mocks.execute.mockImplementation(async ({ request }: { request: SelectorRequest }) => {
+ if (request.kind === 'detail') return { kind: 'detail', item: null }
+ if (request.cursor) {
+ if (failure === 'error') throw new Error('Provider is unavailable')
+ return { kind: 'list', items: [options[1]], truncated: true }
+ }
+ return { kind: 'list', items: [options[0]], nextCursor: 'next' }
+ })
+ const view = await renderBulkSelector()
+ try {
+ await act(async () => view.all().onSelect?.())
+ await act(async () =>
+ vi.waitFor(() => expect(view.container.querySelector('[role="alert"]')).not.toBeNull(), {
+ interval: 1,
+ })
+ )
+ await view.rerender()
+ expect(view.container.querySelector('[role="alert"]')).not.toBeNull()
+ expect(view.change).not.toHaveBeenCalled()
+ expect(view.container.textContent).not.toContain('Select all')
+ expect(view.container.textContent).toContain(
+ failure === 'error' ? 'Could not load all options' : 'too many results'
+ )
+ await view.rerender({ domain: 'another.atlassian.net' })
+ expect(view.container.querySelector('[role="alert"]')).toBeNull()
+ } finally {
+ await view.dispose()
+ }
+})
+
+it('disables bulk selection while searching and ignores a completion after the user edits selection', async () => {
+ let resolvePage!: (result: SelectorResult) => void
+ const pending = new Promise((resolve) => {
+ resolvePage = resolve
+ })
+ mocks.execute.mockImplementation(({ request }: { request: SelectorRequest }) =>
+ request.kind === 'detail'
+ ? Promise.resolve({ kind: 'detail', item: null })
+ : request.cursor
+ ? pending
+ : Promise.resolve({ kind: 'list', items: [options[0]], nextCursor: 'next' })
+ )
+ const view = await renderBulkSelector()
+ try {
+ await act(async () => mocks.combobox.mock.lastCall![0].onSearchChange?.('Eng'))
+ expect(view.all().disabled).toBe(true)
+ await act(async () => mocks.combobox.mock.lastCall![0].onSearchChange?.(''))
+ await act(async () => view.all().onSelect?.())
+ await act(async () => mocks.combobox.mock.lastCall![0].onMultiSelectChange?.([]))
+ expect(view.change).toHaveBeenCalledTimes(1)
+ await act(async () => resolvePage({ kind: 'list', items: [options[1]] }))
+ await act(async () =>
+ vi.waitFor(() => expect(view.all().disabled).toBe(false), {
+ interval: 1,
+ })
+ )
+ expect(view.change).toHaveBeenCalledTimes(1)
+ expect(view.change).toHaveBeenLastCalledWith([], [])
+ } finally {
+ await view.dispose()
+ }
+})
+
+it('hydrates only the trigger-visible labels for large saved selections', async () => {
+ const client = new QueryClient({ defaultOptions: { queries: { retry: false } } })
+ const root = createRoot(document.createElement('div'))
+ try {
+ await act(async () =>
+ root.render(
+
+ `SAVED-${index}`)}
+ onChange={vi.fn()}
+ credentialId='credential-1'
+ sourceConfig={{ domain: 'example.atlassian.net' }}
+ configFields={[domainField, field]}
+ canonicalModes={{}}
+ />
+
+ )
+ )
+ await act(async () =>
+ vi.waitFor(() => expect(mocks.combobox.mock.lastCall![0].isLoading).toBe(false), {
+ interval: 1,
+ })
+ )
+ expect(
+ mocks.execute.mock.calls
+ .filter(([args]) => args.request.kind === 'detail')
+ .map(([args]) => args.request.id)
+ ).toEqual(['SAVED-0', 'SAVED-1'])
+ } finally {
+ await act(async () => root.unmount())
+ client.clear()
+ }
+})
diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-selector-field/connector-selector-field.test.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-selector-field/connector-selector-field.test.tsx
index a9746766ade..716589573d0 100644
--- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-selector-field/connector-selector-field.test.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-selector-field/connector-selector-field.test.tsx
@@ -8,9 +8,12 @@ const mocks = vi.hoisted(() => ({
combobox: vi.fn((_props: ComboboxCallbacks) => null),
change: vi.fn(),
projectContext: vi.fn((_key: string, context: Record) => context),
+ loadAll: vi.fn(),
}))
-vi.mock('@sim/emcn', () => ({ ChipCombobox: mocks.combobox }))
+vi.mock('@sim/emcn', () => ({
+ ChipCombobox: mocks.combobox,
+}))
vi.mock('next/navigation', () => ({ useParams: () => ({ workspaceId: 'workspace-1' }) }))
vi.mock('@/lib/selectors/context', () => ({ projectSelectorContext: mocks.projectContext }))
vi.mock('@/lib/selectors/manifest', () => ({
@@ -22,6 +25,7 @@ vi.mock('@/hooks/queries/selectors', () => ({
data: [{ id: 'folder-b', label: 'Company docs', secret: 'not display metadata' }],
error: null,
truncated: false,
+ loadAll: mocks.loadAll,
}),
useSelectorOptionDetails: () => ({ data: [{ id: 'folder-a', label: 'Engineering' }] }),
useSelectorOptionDetail: () => ({}),
@@ -30,7 +34,7 @@ vi.mock('@/hooks/queries/selectors', () => ({
import { ConnectorSelectorField } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/connector-selector-field/connector-selector-field'
interface ComboboxCallbacks {
- options: { value: string; label: string; hidden?: boolean }[]
+ options: { value: string; label: string; hidden?: boolean; onSelect?: () => void }[]
disabled: boolean
onChange?: (value: string) => void
onMultiSelectChange?: (value: string[]) => void
@@ -173,3 +177,55 @@ it('uses saved labels only for selected values and prefers live provider names',
vi.clearAllMocks()
}
})
+
+it('keeps the prior selection when all personal setup options exceed its source limit', async () => {
+ mocks.loadAll.mockResolvedValue({
+ status: 'complete',
+ options: Array.from({ length: 1001 }, (_, index) => ({
+ id: `P${index}`,
+ label: `Project ${index}`,
+ })),
+ })
+ const field: ConnectorConfigField & { selectorKey: 'jira.projectKeys' } = {
+ id: 'projects',
+ title: 'Projects',
+ type: 'selector',
+ selectorKey: 'jira.projectKeys',
+ multi: true,
+ allowSelectAll: true,
+ }
+ const container = document.createElement('div')
+ const root = createRoot(container)
+ try {
+ await act(async () =>
+ root.render(
+
+ )
+ )
+ const all = mocks.combobox.mock.lastCall![0].options.find((item) => item.label === 'All')
+ expect(container.textContent).not.toContain('Select all')
+ await act(async () => all?.onSelect?.())
+ expect(mocks.loadAll).toHaveBeenCalledTimes(1)
+ expect(mocks.change).not.toHaveBeenCalled()
+ expect(container.querySelector('[role="alert"]')?.textContent).toContain(
+ 'Select up to 1,000 items'
+ )
+ } finally {
+ await act(async () => root.unmount())
+ vi.clearAllMocks()
+ }
+})
diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-selector-field/connector-selector-field.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-selector-field/connector-selector-field.tsx
index 8a02e6a09d1..bdd170f7e50 100644
--- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-selector-field/connector-selector-field.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-selector-field/connector-selector-field.tsx
@@ -1,12 +1,14 @@
'use client'
-import { useMemo, useState } from 'react'
-import { ChipCombobox, type ComboboxOption } from '@sim/emcn'
+import { useEffect, useMemo, useRef, useState } from 'react'
+import { ChipCombobox, type ChipModalFieldAria, type ComboboxOption } from '@sim/emcn'
+import { isEqual } from 'es-toolkit'
import { useParams } from 'next/navigation'
import { type ResourceScope, resourceScopeFromOwner } from '@/lib/core/resource-scope'
import { projectSelectorContext } from '@/lib/selectors/context'
import { getSelectorManifestEntry, type SelectorKey } from '@/lib/selectors/manifest'
-import type { SelectorContext } from '@/lib/selectors/types'
+import type { SelectorContext, SelectorSurface } from '@/lib/selectors/types'
+import { MAX_PERSONAL_SOURCE_SETUP_KEYS } from '@/lib/sim-search/personal-source-setup'
import type { SourceSelectionLabel } from '@/lib/sim-search/source-identity'
import { SEARCH_DEBOUNCE_MS } from '@/lib/url-state'
import { getDependsOnFields } from '@/lib/workflows/subblocks/dependencies'
@@ -23,7 +25,9 @@ import {
import { useDebounce } from '@/hooks/use-debounce'
interface ConnectorSelectorFieldProps {
+ controlAria?: ChipModalFieldAria
scope?: ResourceScope
+ selectorSurface?: SelectorSurface
field: ConnectorConfigField & { selectorKey: SelectorKey }
value: ConfigFieldValue
onChange: (value: ConfigFieldValue, selectedOptions?: SourceSelectionLabel[]) => void
@@ -37,7 +41,9 @@ interface ConnectorSelectorFieldProps {
}
export function ConnectorSelectorField({
+ controlAria,
scope: explicitScope,
+ selectorSurface,
field,
value,
onChange,
@@ -53,6 +59,17 @@ export function ConnectorSelectorField({
const scope = explicitScope ?? resourceScopeFromOwner(params)
const isMulti = Boolean(field.multi)
const [searchTerm, setSearchTerm] = useState('')
+ const [bulkError, setBulkError] = useState<{ context: SelectorContext; message: string } | null>(
+ null
+ )
+ const bulkGenerationRef = useRef(0)
+
+ useEffect(
+ () => () => {
+ bulkGenerationRef.current += 1
+ },
+ []
+ )
const context = useMemo(() => {
const candidate: Record = {}
@@ -95,20 +112,26 @@ export function ConnectorSelectorField({
}, [field.dependsOn, sourceConfig, configFields, canonicalModes])
const isEnabled = !disabled && !!credentialId && depsResolved
+ const missingDependencyMessage = selectorSurface
+ ? 'Enter your Atlassian site first'
+ : `Select ${getDependencyLabel(field, configFields)} first`
const debouncedSearch = useDebounce(searchTerm.trim(), SEARCH_DEBOUNCE_MS)
const {
data: options = [],
isLoading,
+ isFetching,
hasMore,
isFetchingMore,
isLoadingAll,
truncated,
loadMore,
loadAll,
+ refetch,
error,
} = useSelectorOptions(field.selectorKey, {
context,
scope,
+ surface: selectorSurface,
search: debouncedSearch,
enabled: isEnabled,
surfaceId: `connector:${field.id}`,
@@ -121,13 +144,15 @@ export function ConnectorSelectorField({
)
const missingSelectedIds = useMemo(() => {
const loadedIds = new Set(options.map((option) => option.id))
- return selectedIds.filter((id) => !loadedIds.has(id))
+ /** The trigger displays at most two labels; additional selections are counted. */
+ return selectedIds.slice(0, 2).filter((id) => !loadedIds.has(id))
}, [options, selectedIds])
const { data: selectedOptions, isLoading: isLoadingSelectedOptions } = useSelectorOptionDetails(
field.selectorKey,
{
context,
scope,
+ surface: selectorSurface,
detailIds: isEnabled ? missingSelectedIds : [],
surfaceId: `connector:${field.id}`,
}
@@ -143,6 +168,7 @@ export function ConnectorSelectorField({
const { data: searchedOption } = useSelectorOptionDetail(field.selectorKey, {
context,
scope,
+ surface: selectorSurface,
detailId:
resolvesUnknownIds && isEnabled && debouncedSearch.length > 0 ? debouncedSearch : undefined,
surfaceId: `connector:${field.id}`,
@@ -171,6 +197,8 @@ export function ConnectorSelectorField({
}, [options, selectedOptions, searchedOption, selectedLabels, selectedIds])
const handleChange = (nextValue: ConfigFieldValue) => {
+ bulkGenerationRef.current += 1
+ setBulkError(null)
const ids = new Set(Array.isArray(nextValue) ? nextValue : nextValue ? [nextValue] : [])
const selected = comboboxOptions
.filter((option) => ids.has(option.value))
@@ -178,61 +206,138 @@ export function ConnectorSelectorField({
onChange(nextValue, selected)
}
+ const handleSearchChange = (nextSearch: string) => {
+ bulkGenerationRef.current += 1
+ setBulkError(null)
+ setSearchTerm(nextSearch)
+ }
+
+ const hasSearch = searchTerm.trim().length > 0 || debouncedSearch.length > 0
+ const selectedIdSet = new Set(selectedIds)
+ const allSelected =
+ !hasMore &&
+ !truncated &&
+ options.length > 0 &&
+ selectedIds.length === options.length &&
+ options.every((option) => selectedIdSet.has(option.id))
+ const selectAll = async () => {
+ if (!isEnabled || hasSearch || isFetching || isLoadingAll) return
+ if (allSelected) {
+ handleChange([])
+ return
+ }
+ const generation = ++bulkGenerationRef.current
+ setBulkError(null)
+ const result = await loadAll()
+ if (bulkGenerationRef.current !== generation || result.status === 'cancelled') return
+ if (result.status !== 'complete') {
+ setBulkError({
+ context,
+ message:
+ result.status === 'partial'
+ ? 'There are too many results to select all. Select items individually or enter keys.'
+ : 'Could not load all options. Try again.',
+ })
+ return
+ }
+ if (
+ selectorSurface?.kind === 'personal-search-setup' &&
+ result.options.length > MAX_PERSONAL_SOURCE_SETUP_KEYS
+ ) {
+ setBulkError({
+ context,
+ message: `Select up to ${MAX_PERSONAL_SOURCE_SETUP_KEYS.toLocaleString()} items. Choose a smaller set to continue.`,
+ })
+ return
+ }
+ onChange(
+ result.options.map((option) => option.id),
+ result.options.map((option) => ({ id: option.id, label: option.label }))
+ )
+ }
+
if (isMulti) {
const multiValues = Array.isArray(value) ? value : value ? [value] : []
return (
-
+
+
0 || hasMore)
+ ? [
+ {
+ value: '',
+ label: 'All',
+ disabled: !isEnabled || hasSearch || isFetching || isLoadingAll,
+ onSelect: () => void selectAll(),
+ keepOpen: true,
+ selected: allSelected,
+ },
+ ...comboboxOptions,
+ ]
+ : comboboxOptions
+ }
+ multiSelectValues={multiValues}
+ onMultiSelectChange={handleChange}
+ searchable
+ onSearchChange={handleSearchChange}
+ searchPlaceholder={`Search ${field.title.toLowerCase()}...`}
+ placeholder={
+ !credentialId
+ ? 'Connect an account first'
+ : !depsResolved
+ ? missingDependencyMessage
+ : field.placeholder || `Select ${field.title.toLowerCase()}`
+ }
+ disabled={disabled || !credentialId || !depsResolved}
+ isLoading={isEnabled && (isLoading || (options.length === 0 && isLoadingSelectedOptions))}
+ hasMore={hasMore || Boolean(error)}
+ isLoadingMore={isFetchingMore}
+ isLoadingAll={isLoadingAll}
+ truncated={truncated}
+ onLoadMore={error ? refetch : loadMore}
+ onLoadAll={error ? refetch : loadAll}
+ emptyMessage={emptyMessage}
+ error={error?.message}
+ />
+ {bulkError && isEqual(bulkError.context, context) && (
+
+ {bulkError.message}
+
+ )}
+
)
}
return (
)
}
@@ -244,7 +349,7 @@ function getEmptyMessage(
truncated: boolean
}
): string {
- if (state.error) return 'No match — the list failed to load. Try reopening'
+ if (state.error) return 'Could not load options. Try again.'
if (state.truncated) return 'No match — too many to list. Try a more exact term'
return `No ${noun} found`
}
diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-selector-field/connector-selector-field.ui.test.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-selector-field/connector-selector-field.ui.test.tsx
new file mode 100644
index 00000000000..2f966afe39d
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-selector-field/connector-selector-field.ui.test.tsx
@@ -0,0 +1,179 @@
+/** @vitest-environment jsdom */
+import { act, useState } from 'react'
+import { createRoot } from 'react-dom/client'
+import { beforeEach, expect, it, vi } from 'vitest'
+
+const mocks = vi.hoisted(() => ({
+ loadAll: vi.fn(),
+ change: vi.fn(),
+ hasMore: true,
+ options: null as { id: string; label: string }[] | null,
+ error: null as Error | null,
+ refetch: vi.fn(),
+ loadMore: vi.fn(),
+}))
+vi.mock('next/navigation', () => ({ useParams: () => ({ organizationId: 'org-1' }) }))
+vi.mock('@/hooks/queries/selectors', () => ({
+ useSelectorOptions: () => ({
+ data:
+ mocks.options ??
+ (mocks.hasMore
+ ? [{ id: 'ENG', label: 'Engineering' }]
+ : [
+ { id: 'ENG', label: 'Engineering' },
+ { id: 'OPS', label: 'Operations' },
+ ]),
+ hasMore: mocks.hasMore,
+ isLoading: false,
+ isFetching: false,
+ isLoadingAll: false,
+ truncated: false,
+ loadAll: mocks.loadAll,
+ loadMore: mocks.loadMore,
+ refetch: mocks.refetch,
+ error: mocks.error,
+ }),
+ useSelectorOptionDetails: () => ({ data: [] }),
+ useSelectorOptionDetail: () => ({}),
+}))
+
+import { ConnectorSelectorField } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/connector-selector-field/connector-selector-field'
+
+function ControlledSelector() {
+ const [value, setValue] = useState([])
+ return (
+ {
+ mocks.change(next, labels)
+ mocks.hasMore = false
+ setValue(Array.isArray(next) ? next : [])
+ }}
+ sourceConfig={{}}
+ configFields={[]}
+ canonicalModes={{}}
+ />
+ )
+}
+
+beforeEach(() => {
+ vi.clearAllMocks()
+ mocks.hasMore = true
+ mocks.options = null
+ mocks.error = null
+})
+
+it('selects all pages with the keyboard, announces selection, and toggles it off', async () => {
+ vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true)
+ const originalScroll = HTMLElement.prototype.scrollIntoView
+ HTMLElement.prototype.scrollIntoView = vi.fn()
+ mocks.loadAll.mockResolvedValue({
+ status: 'complete',
+ options: [
+ { id: 'ENG', label: 'Engineering' },
+ { id: 'OPS', label: 'Operations' },
+ ],
+ })
+ const container = document.createElement('div')
+ document.body.appendChild(container)
+ const root = createRoot(container)
+ try {
+ await act(async () => root.render( ))
+ expect(container.textContent).not.toContain('Select all')
+ expect(container.textContent).not.toContain('Clear')
+ const trigger = container.querySelector('[role="combobox"]')
+ expect(trigger).not.toBeNull()
+ await act(async () =>
+ trigger?.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }))
+ )
+ expect(document.querySelector('[role="option"]')?.textContent).toBe('All')
+ await act(async () =>
+ trigger?.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))
+ )
+ expect(document.querySelector('[role="option"]')?.getAttribute('aria-selected')).toBe('true')
+ expect(mocks.loadAll).toHaveBeenCalledOnce()
+ expect(mocks.change).toHaveBeenCalledWith(
+ ['ENG', 'OPS'],
+ [
+ { id: 'ENG', label: 'Engineering' },
+ { id: 'OPS', label: 'Operations' },
+ ]
+ )
+ await act(async () =>
+ document
+ .querySelector('[role="option"]')
+ ?.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }))
+ )
+ expect(mocks.change).toHaveBeenLastCalledWith([], [])
+ expect(document.querySelector('[role="option"]')?.getAttribute('aria-selected')).toBe('false')
+ expect(mocks.loadAll).toHaveBeenCalledOnce()
+ } finally {
+ await act(async () => root.unmount())
+ container.remove()
+ HTMLElement.prototype.scrollIntoView = originalScroll
+ vi.unstubAllGlobals()
+ }
+})
+
+it.each(['empty', 'error'] as const)(
+ 'shows the initial %s state without All and allows failed lists to retry',
+ async (state) => {
+ vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true)
+ const originalScroll = HTMLElement.prototype.scrollIntoView
+ HTMLElement.prototype.scrollIntoView = vi.fn()
+ mocks.options = []
+ mocks.hasMore = false
+ mocks.error = state === 'error' ? new Error('Provider unavailable') : null
+ const container = document.createElement('div')
+ document.body.appendChild(container)
+ const root = createRoot(container)
+ try {
+ await act(async () => root.render( ))
+ const trigger = container.querySelector('[role="combobox"]')
+ expect(trigger).not.toBeNull()
+ await act(async () => trigger?.click())
+ expect(document.querySelector('[role="listbox"]')).not.toBeNull()
+ expect(document.querySelector('[role="option"]')).toBeNull()
+ expect(mocks.loadAll).not.toHaveBeenCalled()
+
+ const retry = Array.from(document.querySelectorAll('button')).find(
+ (button) => button.textContent?.trim() === 'Try again'
+ )
+ if (state === 'empty') {
+ expect(document.body.textContent).toContain('No spaces found')
+ expect(retry).toBeUndefined()
+ return
+ }
+ expect(document.body.textContent).toContain('Provider unavailable')
+ expect(retry).toBeDefined()
+ await act(async () => retry?.click())
+ expect(mocks.refetch).toHaveBeenCalledOnce()
+ expect(mocks.loadMore).not.toHaveBeenCalled()
+ mocks.error = null
+ mocks.options = [{ id: 'ENG', label: 'Engineering' }]
+ await act(async () => root.render( ))
+ expect(
+ Array.from(document.querySelectorAll('[role="option"]')).map((option) => option.textContent)
+ ).toEqual(['All', 'Engineering'])
+ expect(
+ Array.from(document.querySelectorAll('button')).some(
+ (button) => button.textContent?.trim() === 'Try again'
+ )
+ ).toBe(false)
+ } finally {
+ await act(async () => root.unmount())
+ container.remove()
+ HTMLElement.prototype.scrollIntoView = originalScroll
+ vi.unstubAllGlobals()
+ }
+ }
+)
diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connector-actions.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connector-actions.tsx
index cc2e15757d1..ac30124057f 100644
--- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connector-actions.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connector-actions.tsx
@@ -73,11 +73,11 @@ export function ConnectorActionFeedback({ state }: ConnectorActionFeedbackProps)
{
},
]
await render(confluenceConnectorMeta)
- expect(container.textContent).toContain('Indexing account')
+ expect(container.textContent).toContain('Service account')
await openAccountChoices()
expect(
Array.from(document.querySelectorAll('[role="option"]')).map((node) =>
diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/connector-settings-fields.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/connector-settings-fields.tsx
index 6e2f1c9612a..ee36b6d49de 100644
--- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/connector-settings-fields.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/connector-settings-fields.tsx
@@ -258,7 +258,11 @@ export function ConnectorSettingsFields({
{isSwitchingAccess
? 'Switching…'
: isContentCredentialChange
- ? 'Change indexing account'
+ ? isSearchIndex
+ ? requiresServiceAccount
+ ? 'Change service account'
+ : 'Change account'
+ : 'Change indexing account'
: 'Apply connection method'}
@@ -269,7 +273,7 @@ export function ConnectorSettingsFields({
{accessSetupHint ??
(isContentCredentialChange
? syncsPerMember
- ? 'The next sync uses this indexing account. Members keep their connected accounts and source permissions.'
+ ? 'The next sync uses this account. Members keep their connected accounts and source permissions.'
: 'The next sync uses this account and refreshes source permissions.'
: SWITCH_NOTICE[access.accessMode])}
@@ -282,7 +286,13 @@ export function ConnectorSettingsFields({
{connectorConfig && needsWorkspaceCredential && canAdmin && (
({
GmailIcon: () => null,
GoogleCalendarIcon: () => null,
GoogleDriveIcon: () => null,
+ JiraIcon: () => null,
+ ConfluenceIcon: () => null,
}))
import { describeSearchSource, SOURCE_LABELS_KEY } from '@/lib/sim-search/source-identity'
@@ -17,9 +19,11 @@ import {
type UseConnectorConfigFieldsResult,
useConnectorConfigFields,
} from '@/app/workspace/[workspaceId]/knowledge/[id]/hooks/use-connector-config-fields'
+import { confluenceConnectorMeta } from '@/connectors/confluence/meta'
import { gmailConnectorMeta } from '@/connectors/gmail/meta'
import { googleCalendarConnectorMeta } from '@/connectors/google-calendar/meta'
import { googleDriveConnectorMeta } from '@/connectors/google-drive/meta'
+import { jiraConnectorMeta } from '@/connectors/jira/meta'
import type { ConnectorMeta } from '@/connectors/types'
describe('useConnectorConfigFields member configuration', () => {
@@ -277,6 +281,36 @@ describe('useConnectorConfigFields member configuration', () => {
expect(current.selectionLabels).toEqual({})
})
+ it.each([
+ [jiraConnectorMeta, 'projectKey', 'projectSelector'],
+ [confluenceConnectorMeta, 'spaceKey', 'spaceSelector'],
+ ] as const)(
+ 'carries the current $0.name keys between picker and manual entry',
+ (meta, canonicalId, selectorId) => {
+ render({ connectorConfig: meta, accessMode: 'members' })
+ act(() => current.handleFieldChange('domain', 'team.atlassian.net'))
+ act(() =>
+ current.handleFieldChange(
+ selectorId,
+ ['ENG', 'SUPPORT'],
+ [{ id: 'ENG', label: 'Engineering' }]
+ )
+ )
+ act(() => current.toggleCanonicalMode(canonicalId))
+ expect(current.resolveSourceConfig()[canonicalId]).toEqual(['ENG', 'SUPPORT'])
+ expect(current.selectionLabels[canonicalId]).toEqual([{ id: 'ENG', label: 'Engineering' }])
+ act(() => current.handleFieldChange(canonicalId, 'ENG, PRODUCT'))
+ act(() => current.toggleCanonicalMode(canonicalId))
+ expect(current.sourceConfig[selectorId]).toEqual(['ENG', 'PRODUCT'])
+ expect(current.resolveSourceConfig()[canonicalId]).toEqual(['ENG', 'PRODUCT'])
+ expect(current.selectionLabels).toEqual({})
+ act(() => current.handleFieldChange('domain', 'other.atlassian.net'))
+ expect(current.resolveSourceConfig()[canonicalId]).toEqual([])
+ act(() => current.toggleCanonicalMode(canonicalId))
+ expect(current.resolveSourceConfig()[canonicalId]).toEqual([])
+ }
+ )
+
it('clears dependent selector labels together with their values', () => {
const meta: ConnectorMeta = {
...googleDriveConnectorMeta,
diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/hooks/use-connector-config-fields.ts b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/hooks/use-connector-config-fields.ts
index 6d8619ec5fa..1308e9c7e2c 100644
--- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/hooks/use-connector-config-fields.ts
+++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/hooks/use-connector-config-fields.ts
@@ -219,17 +219,29 @@ export function useConnectorConfigFields({
[dependentFieldIds, fieldsById]
)
- const toggleCanonicalMode = useCallback((canonicalId: string) => {
- setSelectionLabels((prev) => {
- const next = { ...prev }
- delete next[canonicalId]
- return next
- })
- setCanonicalModes((prev) => ({
- ...prev,
- [canonicalId]: prev[canonicalId] === 'advanced' ? 'basic' : 'advanced',
- }))
- }, [])
+ const toggleCanonicalMode = useCallback(
+ (canonicalId: string) => {
+ const group = canonicalGroups.get(canonicalId) ?? []
+ const currentMode = canonicalModes[canonicalId] ?? 'basic'
+ const nextMode = currentMode === 'basic' ? 'advanced' : 'basic'
+ const currentField = group.find((field) => field.mode === currentMode)
+ const nextField = group.find((field) => field.mode === nextMode)
+ if (currentField && nextField && group.some((field) => field.preserveValueOnModeChange)) {
+ setFieldValues((prev) => ({
+ ...prev,
+ [nextField.id]: coerceForField(nextField, prev[currentField.id]),
+ }))
+ } else {
+ setSelectionLabels((prev) => {
+ const next = { ...prev }
+ delete next[canonicalId]
+ return next
+ })
+ }
+ setCanonicalModes((prev) => ({ ...prev, [canonicalId]: nextMode }))
+ },
+ [canonicalGroups, canonicalModes]
+ )
const resolveSourceConfig = useCallback((): Record => {
const resolved: Record = {}
diff --git a/apps/sim/app/workspace/[workspaceId]/search/components/search-source-pagination.tsx b/apps/sim/app/workspace/[workspaceId]/search/components/search-source-pagination.tsx
index eadc0096981..d477bb14a4a 100644
--- a/apps/sim/app/workspace/[workspaceId]/search/components/search-source-pagination.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/search/components/search-source-pagination.tsx
@@ -23,7 +23,7 @@ export function SearchSourcePagination({
return (
void fetchNextPage()}
variant='inline'
diff --git a/apps/sim/app/workspace/[workspaceId]/search/components/search-source-setup.test.tsx b/apps/sim/app/workspace/[workspaceId]/search/components/search-source-setup.test.tsx
index 41900dd4c2a..90bb6dafcf2 100644
--- a/apps/sim/app/workspace/[workspaceId]/search/components/search-source-setup.test.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/search/components/search-source-setup.test.tsx
@@ -375,16 +375,16 @@ function organizationSetup() {
describe('organization setup entry points', () => {
it.each([
- { type: 'google_drive', mode: 'admin', name: 'Google Drive' },
- { type: 'gmail', mode: 'admin', name: 'Gmail' },
- { type: 'google_calendar', mode: 'admin', name: 'Google Calendar' },
- { type: 'confluence', mode: 'admin', name: 'Confluence' },
- { type: 'gitlab', mode: 'admin', name: 'GitLab' },
- { type: 'gmail', mode: 'members', name: 'Gmail' },
- { type: 'google_calendar', mode: 'members', name: 'Google Calendar' },
- { type: 'jira', mode: 'members', name: 'Jira' },
- { type: 'github', mode: 'members', name: 'GitHub' },
- { type: 'slack', mode: 'members', name: 'Slack' },
+ { type: 'google_drive', mode: 'admin', name: 'Connect Google Drive service account' },
+ { type: 'gmail', mode: 'admin', name: 'Connect Gmail service account' },
+ { type: 'google_calendar', mode: 'admin', name: 'Connect Google Calendar service account' },
+ { type: 'confluence', mode: 'admin', name: 'Connect Confluence site' },
+ { type: 'gitlab', mode: 'admin', name: 'Add GitLab project' },
+ { type: 'gmail', mode: 'members', name: 'Set up Gmail member accounts' },
+ { type: 'google_calendar', mode: 'members', name: 'Set up Google Calendar member accounts' },
+ { type: 'jira', mode: 'members', name: 'Add Jira projects' },
+ { type: 'github', mode: 'members', name: 'Add GitHub repository' },
+ { type: 'slack', mode: 'members', name: 'Choose Slack channels and DMs' },
])(
'opens the known $name configuration after preparing its missing index',
async ({ type, mode, name }) => {
@@ -396,32 +396,36 @@ describe('organization setup entry points', () => {
connectorType: type,
accessMode: mode,
})
- expect(document.body.textContent).toContain(`Configure ${name}`)
+ expect(document.querySelector('[role="dialog"]')).toBeNull()
expect(document.body.textContent).not.toContain('Continue setup')
expect(document.body.textContent).not.toContain('Find a source')
+ mocks.preparePending = true
await render(organizationSetup(), query)
expect(mocks.prepare).toHaveBeenCalledOnce()
+ expect(document.querySelector('[role="dialog"]')).toBeNull()
+ mocks.preparePending = false
mocks.bases = [{ id: 'kb-search', name: 'Sim Search', isSearchIndex: true }]
await render(organizationSetup(), query)
- expect(document.body.textContent).toContain(`Configure ${name}`)
+ expect(document.body.textContent).toContain(name)
expect(document.body.textContent).not.toContain('Loading source setup')
expect(document.querySelector('button[aria-label="Choose another source"]')).toBeNull()
expect(mocks.prepare).toHaveBeenCalledOnce()
}
)
- it('waits for index discovery without showing a redundant provider row', async () => {
+ it('waits for index discovery without opening an interim modal', async () => {
mocks.bases = []
mocks.basesPending = true
await render(organizationSetup(), '?addConnector=google_drive')
expect(mocks.prepare).not.toHaveBeenCalled()
- expect(document.body.textContent).toContain('Loading source setup')
+ expect(document.querySelector('[role="dialog"]')).toBeNull()
expect(document.body.textContent).not.toContain('Continue setup')
mocks.basesPending = false
mocks.bases = [{ id: 'kb-search', name: 'Sim Search', isSearchIndex: true }]
await render(organizationSetup(), '?addConnector=google_drive')
expect(mocks.prepare).not.toHaveBeenCalled()
expect(document.body.textContent).not.toContain('Loading source setup')
+ expect(document.body.textContent).toContain('Connect Google Drive service account')
})
it.each(['loading', 'error'] as const)(
@@ -434,6 +438,11 @@ describe('organization setup entry points', () => {
await render(organizationSetup(), '?addConnector=google_drive')
expect(mocks.prepare).not.toHaveBeenCalled()
expect(document.body.textContent).not.toContain('Continue setup')
+ if (state === 'loading') {
+ expect(document.querySelector('[role="dialog"]')).toBeNull()
+ } else {
+ expect(document.body.textContent).toContain('Availability failed')
+ }
}
)
@@ -467,7 +476,7 @@ describe('organization setup entry points', () => {
expect(mocks.prepare).not.toHaveBeenCalled()
})
- it('uses central mode and its own draft even when the saved draft contains member mode', async () => {
+ it('honors the central entry point while restoring its draft and offering both sync methods', async () => {
mocks.credentials = [
{
id: 'cred-source',
@@ -478,7 +487,7 @@ describe('organization setup entry points', () => {
]
useConnectorSetupStore
.getState()
- .saveDraft('user-1:organization:org-1:kb-search:confluence:admin', {
+ .saveDraft('user-1:organization:org-1:kb-search:confluence:choose', {
sourceConfig: { domain: 'team.atlassian.net', spaceKey: ['ENG'] },
canonicalModes: { spaceKey: 'advanced' },
accessMode: 'members',
@@ -489,7 +498,8 @@ describe('organization setup entry points', () => {
})
await render(organizationSetup(), '?addConnector=confluence')
- expect(document.body.textContent).not.toContain('Sync using')
+ expect(button('Member accounts')).toBeEnabled()
+ expect(button('Service account')).toBeEnabled()
expect(document.querySelector('button[aria-label="Choose another source"]')).toBeNull()
expect(
document.querySelector('input[placeholder="yoursite.atlassian.net"]')?.value
@@ -548,12 +558,16 @@ describe('organization setup entry points', () => {
mocks.bases = [{ id: 'kb-search', name: 'Sim Search', isSearchIndex: true }]
await render(organizationSetup(), `?addConnector=${type}`)
expect(button('Connect & Sync')).toBeDisabled()
- expect(document.body.textContent).not.toContain('Sync using')
+ expect(document.body.textContent).toContain('Sync using')
+ expect(document.querySelector('button[aria-label="Choose another source"]')).toBeNull()
+ await click(button('Member accounts'))
+ expect(button('Set up member accounts')).toBeEnabled()
+ expect(document.body.textContent).not.toContain('Directory administrator email')
await render(organizationSetup(), `?addConnector=${type}&source-access=members`)
- expect(button('Add source')).toBeEnabled()
+ expect(button('Set up member accounts')).toBeEnabled()
expect(document.body.textContent).not.toContain('Connect & Sync')
expect(document.body.textContent).not.toContain('Directory administrator email')
- await click(button('Add source'))
+ await click(button('Set up member accounts'))
expect(mocks.create).toHaveBeenCalledWith(
expect.objectContaining({
connectorType: type,
@@ -614,14 +628,86 @@ describe('organization setup entry points', () => {
}
)
- it('remounts the locked form when the same provider URL changes connection mode', async () => {
+ it('resets the form to the requested method when the same provider URL changes connection mode', async () => {
await render(organizationSetup(), '?addConnector=confluence&source-access=members')
- expect(button('Add source')).toBeDisabled()
+ expect(button('Add Confluence site')).toBeDisabled()
await render(organizationSetup(), '?addConnector=confluence')
expect(button('Connect & Sync')).toBeDisabled()
expect(document.body.textContent).not.toContain('Add source')
- expect(document.body.textContent).not.toContain('Sync using')
+ expect(button('Member accounts')).toBeEnabled()
+ expect(button('Service account')).toBeEnabled()
})
+
+ it.each([
+ {
+ type: 'gmail',
+ placeholder: 'e.g. INBOX, Engineering (comma-separated)',
+ value: 'QA',
+ config: { label: ['QA'] },
+ manualField: null,
+ },
+ {
+ type: 'google_drive',
+ placeholder: 'e.g. 1aBcDeFg…, 2cDeFgHi… (comma-separated for multiple)',
+ value: 'qa-folder',
+ config: { folderId: ['qa-folder'] },
+ manualField: 'Folders',
+ },
+ {
+ type: 'google_calendar',
+ placeholder: 'e.g. primary, team@group.calendar.google.com (comma-separated for multiple)',
+ value: 'qa@group.calendar.google.com',
+ config: { calendarId: ['qa@group.calendar.google.com'] },
+ manualField: 'Calendars',
+ },
+ {
+ type: 'confluence',
+ placeholder: 'e.g. ENG, PRODUCT (comma-separated for multiple)',
+ value: 'QA',
+ config: { spaceKey: ['QA'], domain: 'qa.atlassian.net' },
+ manualField: 'Spaces',
+ },
+ ])(
+ 'preserves the $type draft and chosen method when availability fails and recovers',
+ async ({ type, placeholder, value, config, manualField }) => {
+ const query = `?addConnector=${type}`
+ await render(organizationSetup(), query)
+ await click(button('Member accounts'))
+ if (type === 'confluence') await fill('yoursite.atlassian.net', 'qa.atlassian.net')
+ if (manualField) await click(button(`Switch ${manualField} to manual input`))
+ await fill(placeholder, value)
+ const submitLabel = type === 'confluence' ? 'Add Confluence site' : 'Set up member accounts'
+ expect(button(submitLabel)).toBeEnabled()
+
+ mocks.availabilityReady = false
+ mocks.availabilityError = new Error('Availability refresh failed')
+ await render(organizationSetup(), query)
+ expect(
+ document.querySelector(`input[placeholder="${placeholder}"]`)?.value
+ ).toBe(value)
+ expect(button(submitLabel)).toBeDisabled()
+ await click(button('Try again'))
+ expect(mocks.refetchAvailability).toHaveBeenCalledOnce()
+ expect(mocks.create).not.toHaveBeenCalled()
+
+ mocks.availabilityReady = true
+ mocks.availabilityError = null
+ await render(organizationSetup(), query)
+ expect(
+ document.querySelector(`input[placeholder="${placeholder}"]`)?.value
+ ).toBe(value)
+ expect(button(submitLabel)).toBeEnabled()
+ await click(button(submitLabel))
+ expect(mocks.create).toHaveBeenCalledWith(
+ expect.objectContaining({
+ connectorType: type,
+ accessMode: 'members',
+ sourceConfig: expect.objectContaining(config),
+ }),
+ expect.any(Object)
+ )
+ }
+ )
})
describe('Search source setup with real connector dialogs', () => {
@@ -1587,9 +1673,9 @@ describe('administrator source prerequisites in real connector dialogs', () => {
)!
await act(async () => option.dispatchEvent(new MouseEvent('mousedown', { bubbles: true })))
expect(button('Save')).toBeDisabled()
- expect(button('Change indexing account')).toBeEnabled()
+ expect(button('Change service account')).toBeEnabled()
- await click(button('Change indexing account'))
+ await click(button('Change service account'))
expect(mocks.applyAccess).toHaveBeenCalledExactlyOnceWith(
{
@@ -1943,7 +2029,7 @@ describe('resuming Search source setup', () => {
await render(setup(), '?search=nothing-matches&addConnector=gitlab&credentialDraftId=draft-1')
expect(document.body.textContent).toContain('Admin or service account')
expect(document.querySelector('[role="radio"][aria-checked="true"]')).toBeNull()
- expect(document.body.textContent).toContain('Configure GitLab')
+ expect(document.body.textContent).toContain('Add GitLab project')
expect(document.body.textContent).not.toContain('Sync Frequency')
expect(document.body.textContent).not.toContain('Sync automatically')
expect(mocks.prepare).not.toHaveBeenCalled()
@@ -1956,7 +2042,7 @@ describe('resuming Search source setup', () => {
expect(document.body.textContent).toContain('Add source')
await fill('Find a source…', 'confluence')
await click(button('Set up'))
- expect(document.body.textContent).toContain('Configure Confluence')
+ expect(document.body.textContent).toContain('Connect Confluence site')
})
it('restores the source configuration and content account after an account-settings detour', async () => {
diff --git a/apps/sim/app/workspace/[workspaceId]/search/components/search-source-setup.tsx b/apps/sim/app/workspace/[workspaceId]/search/components/search-source-setup.tsx
index 9c46ae6d391..81f1c9fb562 100644
--- a/apps/sim/app/workspace/[workspaceId]/search/components/search-source-setup.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/search/components/search-source-setup.tsx
@@ -22,6 +22,7 @@ import {
resourceScopeKey,
} from '@/lib/core/resource-scope'
import { organizationRoutes } from '@/lib/navigation/paths'
+import { getSearchConnectionLabels } from '@/lib/sim-search/connection-labels'
import { getConnectorAccessAvailability, SEARCH_SOURCE_TYPES } from '@/lib/sim-search/connectors'
import { IntegrationTile } from '@/app/workspace/[workspaceId]/integrations/components/integrations-showcase'
import {
@@ -240,10 +241,14 @@ export function SearchSourceSetup({
) {
if (selectedType && session?.user?.id) {
const accessMode = initialMode(selectedType)
- const setupMode = scope.kind === 'organization' ? accessMode : 'choose'
+ const setupMode =
+ scope.kind === 'organization' &&
+ !(selectedMeta?.mirrorsSourceAcls && selectedMeta.auth.mode === 'oauth')
+ ? accessMode
+ : 'choose'
return (
{
if (!nextOpen) void setSelectedType(null)
@@ -253,7 +258,8 @@ export function SearchSourceSetup({
isSearchIndex
initialConnectorType={selectedType}
initialAccessMode={accessMode}
- lockedAccessMode={scope.kind === 'organization' ? accessMode : undefined}
+ lockConnectorType={scope.kind === 'organization'}
+ lockedAccessMode={setupMode === 'choose' ? undefined : setupMode}
setupDraftKey={`${session.user.id}:${resourceScopeKey(scope)}:${knowledgeBaseId}:${selectedType}:${setupMode}`}
onConnectorTypeChange={(type) =>
void setSelectedType(type !== null ? searchSetupParam.parser.parse(type) : null)
@@ -280,6 +286,16 @@ export function SearchSourceSetup({
}
}
+ if (
+ selectedType &&
+ !failedQuery &&
+ !integrationAvailabilityError &&
+ !prepare.error &&
+ (!isIntegrationAvailabilityReady || selectedAvailable)
+ ) {
+ return null
+ }
+
const normalizedSearch = search.trim().toLowerCase()
const visibleTypes = SEARCH_SOURCE_TYPES.filter(
([type, meta]) =>
@@ -300,7 +316,9 @@ export function SearchSourceSetup({
srTitle='Add source'
>
- {selectedMeta ? `Configure ${selectedMeta.name}` : 'Add source'}
+ {selectedType
+ ? getSearchConnectionLabels(selectedType, selectedAccessMode).title
+ : 'Add source'}
{failedQuery ? (
diff --git a/apps/sim/components/charts/bar-chart.tsx b/apps/sim/components/charts/bar-chart.tsx
index 17acfbe4cb9..2e04c6e12e7 100644
--- a/apps/sim/components/charts/bar-chart.tsx
+++ b/apps/sim/components/charts/bar-chart.tsx
@@ -45,6 +45,8 @@ interface BarChartProps {
/** `''` | `'%'` | `'ms'` | `'latency'` | `'credits'` — drives tick and tooltip formatting. */
unit?: string
height?: number
+ /** Display bucket dates in this zone; omitted uses the viewer’s local zone. */
+ timeZone?: string
/** Bucket drawn at full opacity, e.g. the period in progress. */
highlightIndex?: number
}
@@ -75,6 +77,7 @@ function BarChartComponent({
unit,
height = CHART_DEFAULT_HEIGHT,
highlightIndex,
+ timeZone,
}: BarChartProps) {
/*
`useId`, not `useRef(generateShortId())`: a ref initializer is evaluated on
@@ -311,7 +314,7 @@ function BarChartComponent({
textAnchor='middle'
fill={CHART_TICK_FILL}
>
- {Number.isNaN(date.getTime()) ? '' : formatTimeTick(date, spanMs)}
+ {Number.isNaN(date.getTime()) ? '' : formatTimeTick(date, spanMs, timeZone)}
)
})}
@@ -352,7 +355,7 @@ function BarChartComponent({
(() => {
const bar = bars[hoverIndex]
const value = formatBarValue(bar.point.value, unit)
- const date = formatChartTimestamp(bar.point.timestamp)
+ const date = formatChartTimestamp(bar.point.timestamp, timeZone)
const { left, top } = positionChartTooltip({
anchorX: hoverPos?.x ?? bar.x,
anchorY: hoverPos?.y ?? bar.y,
diff --git a/apps/sim/components/charts/chart-format.ts b/apps/sim/components/charts/chart-format.ts
index c5b7151814b..fe31db771d2 100644
--- a/apps/sim/components/charts/chart-format.ts
+++ b/apps/sim/components/charts/chart-format.ts
@@ -16,10 +16,15 @@ export function formatChartLatency(ms: number): string {
}
/** The tooltip's header line: `MAR 4 3:05 PM`. Empty for an unparseable timestamp. */
-export function formatChartTimestamp(timestamp?: string): string {
+export function formatChartTimestamp(timestamp?: string, timeZone?: string): string {
if (!timestamp) return ''
const date = new Date(timestamp)
if (Number.isNaN(date.getTime())) return ''
+ if (timeZone) {
+ const day = date.toLocaleDateString('en-US', { timeZone, month: 'short', day: 'numeric' })
+ const time = date.toLocaleTimeString('en-US', { timeZone, hour: 'numeric', minute: '2-digit' })
+ return `${day.toUpperCase()} ${time}`
+ }
return `${format(date, 'MMM d').toUpperCase()} ${format(date, 'h:mm a')}`
}
diff --git a/apps/sim/components/charts/chart-geometry.test.ts b/apps/sim/components/charts/chart-geometry.test.ts
index 6108dc3386b..735d7f87ac0 100644
--- a/apps/sim/components/charts/chart-geometry.test.ts
+++ b/apps/sim/components/charts/chart-geometry.test.ts
@@ -118,3 +118,9 @@ describe('resolveChartPadding', () => {
expect(padding.bottom).toBe(CHART_PADDING.bottom)
})
})
+
+it('keeps UTC bucket labels independent of the viewer time zone', () => {
+ const bucket = new Date('2026-09-10T00:00:00.000Z')
+ expect(formatTimeTick(bucket, 7 * 24 * 60 * 60 * 1000, 'UTC')).toBe('Sep 10')
+ expect(formatTimeTick(bucket, 7 * 24 * 60 * 60 * 1000, 'America/Los_Angeles')).toBe('Sep 9')
+})
diff --git a/apps/sim/components/charts/chart-geometry.ts b/apps/sim/components/charts/chart-geometry.ts
index 3494543c3c2..9c05f7f36c9 100644
--- a/apps/sim/components/charts/chart-geometry.ts
+++ b/apps/sim/components/charts/chart-geometry.ts
@@ -110,14 +110,19 @@ export function resolveTimeTickIndices(pointCount: number, usableWidth: number):
* Tick label whose precision follows the window: clock time within a day and a half,
* calendar day within a quarter, month beyond that.
*/
-export function formatTimeTick(date: Date, spanMs: number): string {
+export function formatTimeTick(date: Date, spanMs: number, timeZone?: string): string {
if (spanMs <= 36 * 60 * 60 * 1000) {
- return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false })
+ return date.toLocaleTimeString('en-US', {
+ timeZone,
+ hour: '2-digit',
+ minute: '2-digit',
+ hour12: false,
+ })
}
if (spanMs <= 90 * 24 * 60 * 60 * 1000) {
- return date.toLocaleString('en-US', { month: 'short', day: 'numeric' })
+ return date.toLocaleString('en-US', { timeZone, month: 'short', day: 'numeric' })
}
- return date.toLocaleString('en-US', { month: 'short', year: 'numeric' })
+ return date.toLocaleString('en-US', { timeZone, month: 'short', year: 'numeric' })
}
/** Milliseconds between the first and last timestamp, or 0 for a degenerate series. */
diff --git a/apps/sim/components/charts/chart-layout.test.tsx b/apps/sim/components/charts/chart-layout.test.tsx
index 3ab1c4e4bc8..b095d90426d 100644
--- a/apps/sim/components/charts/chart-layout.test.tsx
+++ b/apps/sim/components/charts/chart-layout.test.tsx
@@ -76,6 +76,27 @@ describe('BarChart rendered geometry', () => {
const widths = [280, 420, 680, 1024]
const peaks = [7300, 173_000, 1_234_567]
+ it('renders date buckets and axes for a zero-only series without a no-data overlay', () => {
+ const svg = mountAtWidth(
+ 680,
+
+ )
+ expect(container.textContent).not.toContain('No data')
+ expect(container.textContent).toContain('Jan 1')
+ expect(container.textContent).toContain('Jan 7')
+ expect(container.textContent).not.toContain('Dec 31')
+ expect(svg.querySelectorAll('text').length).toBeGreaterThan(2)
+ expect(svg.querySelectorAll('rect')).toHaveLength(7)
+ expect(svg.querySelectorAll('rect[fill^="url"]')).toHaveLength(0)
+ expect(svg.querySelectorAll('line').length).toBeGreaterThan(0)
+ })
+
it.each(widths.flatMap((width) => peaks.map((peak) => [width, peak] as const)))(
'keeps the y-axis labels inside the box at width %i, peak %i',
(width, peak) => {
diff --git a/apps/sim/connectors/confluence/bulk-spaces.test.ts b/apps/sim/connectors/confluence/bulk-spaces.test.ts
new file mode 100644
index 00000000000..f2e670030a6
--- /dev/null
+++ b/apps/sim/connectors/confluence/bulk-spaces.test.ts
@@ -0,0 +1,234 @@
+/**
+ * @vitest-environment node
+ */
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import { confluenceConnector } from '@/connectors/confluence/confluence'
+
+const fetchMock = vi.fn()
+const config = { domain: 'example.atlassian.net', spaceKey: [] as string[] }
+const keys = Array.from({ length: 120 }, (_, index) => `SPACE_${index}`)
+
+function requestUrl(input: string | URL | Request): URL {
+ return new URL(input instanceof Request ? input.url : String(input))
+}
+
+beforeEach(() => {
+ fetchMock.mockReset()
+ vi.stubGlobal('fetch', fetchMock)
+})
+
+afterEach(() => vi.unstubAllGlobals())
+
+describe('Confluence bulk space validation', () => {
+ it('validates every selected space with bounded query sizes and follows partial pages', async () => {
+ const verifiedKeys: string[] = []
+ fetchMock.mockImplementation(async (input) => {
+ const url = requestUrl(input)
+ const batch = url.searchParams.getAll('keys')
+ expect(batch.length).toBeLessThanOrEqual(50)
+ expect(Number(url.searchParams.get('limit'))).toBeLessThanOrEqual(50)
+ const page = url.searchParams.has('cursor') ? batch.slice(25) : batch.slice(0, 25)
+ verifiedKeys.push(...page)
+ return Response.json({
+ results: page.map((key) => ({ key, id: key })),
+ _links:
+ batch.length > 25 && !url.searchParams.has('cursor')
+ ? { next: '/wiki/api/v2/spaces?cursor=second-page' }
+ : {},
+ })
+ })
+
+ await expect(
+ confluenceConnector.validateConfig(
+ 'token',
+ { ...config, spaceKey: [...keys, keys[0]] },
+ { cloudId: 'cloud' }
+ )
+ ).resolves.toEqual({ valid: true })
+ expect(verifiedKeys).toEqual(keys)
+ expect(fetchMock).toHaveBeenCalledTimes(5)
+ })
+
+ it('bounds encoded URLs for long space keys', async () => {
+ const longKeys = Array.from({ length: 20 }, (_, index) => `SPACE_${index}_${'x'.repeat(240)}`)
+ const verifiedKeys: string[] = []
+ fetchMock.mockImplementation(async (input) => {
+ const url = requestUrl(input)
+ expect(url.toString().length).toBeLessThan(2_100)
+ const batch = url.searchParams.getAll('keys')
+ verifiedKeys.push(...batch)
+ return Response.json({ results: batch.map((key) => ({ key, id: key })) })
+ })
+ await expect(
+ confluenceConnector.validateConfig(
+ 'token',
+ { ...config, spaceKey: longKeys },
+ { cloudId: 'cloud' }
+ )
+ ).resolves.toEqual({ valid: true })
+ expect(verifiedKeys).toEqual(longKeys)
+ expect(fetchMock.mock.calls.length).toBeGreaterThan(1)
+ })
+
+ it('rejects an unavailable space in a later batch', async () => {
+ fetchMock.mockImplementation(async (input) => {
+ const batch = requestUrl(input).searchParams.getAll('keys')
+ return Response.json({
+ results: batch.filter((key) => key !== 'SPACE_110').map((key) => ({ key })),
+ })
+ })
+ await expect(
+ confluenceConnector.validateConfig(
+ 'token',
+ { ...config, spaceKey: keys },
+ { cloudId: 'cloud' }
+ )
+ ).resolves.toEqual({ valid: false, error: 'Space not found: SPACE_110' })
+ })
+
+ it('rejects repeated pagination instead of accepting a partial space set', async () => {
+ fetchMock.mockImplementation(async () =>
+ Response.json({
+ results: [],
+ _links: { next: '/wiki/api/v2/spaces?cursor=repeated' },
+ })
+ )
+ await expect(
+ confluenceConnector.validateConfig(
+ 'token',
+ { ...config, spaceKey: keys },
+ { cloudId: 'cloud' }
+ )
+ ).resolves.toEqual({
+ valid: false,
+ error: 'Confluence returned an incomplete space list. Try again.',
+ })
+ expect(fetchMock).toHaveBeenCalledTimes(2)
+ })
+
+ it('does not accept a later failed batch', async () => {
+ fetchMock.mockImplementation(async (input) => {
+ const batch = requestUrl(input).searchParams.getAll('keys')
+ return batch.includes('SPACE_50')
+ ? Response.json({}, { status: 403 })
+ : Response.json({ results: batch.map((key) => ({ key })) })
+ })
+ await expect(
+ confluenceConnector.validateConfig(
+ 'token',
+ { ...config, spaceKey: keys },
+ { cloudId: 'cloud' }
+ )
+ ).resolves.toEqual({ valid: false, error: 'Failed to validate spaces: 403' })
+ })
+})
+
+describe('Confluence bulk space listing', () => {
+ const sourceConfig = { ...config, spaceKey: keys.slice(0, 52) }
+
+ function page(id: string, next?: string): Response {
+ return Response.json({
+ results: [{ id, title: id, type: 'page', status: 'current', space: { key: 'SPACE_0' } }],
+ _links: next ? { next: `/wiki/rest/api/content/search?cursor=${next}` } : {},
+ })
+ }
+
+ it('exhausts each provider cursor before moving to the next explicit space batch', async () => {
+ fetchMock
+ .mockResolvedValueOnce(page('1', 'provider-page-2'))
+ .mockResolvedValueOnce(page('2'))
+ .mockResolvedValueOnce(page('3'))
+ const context: Record = { cloudId: 'cloud' }
+ const first = await confluenceConnector.listDocuments('token', sourceConfig, undefined, context)
+ const second = await confluenceConnector.listDocuments(
+ 'token',
+ sourceConfig,
+ first.nextCursor,
+ context
+ )
+ const third = await confluenceConnector.listDocuments(
+ 'token',
+ sourceConfig,
+ second.nextCursor,
+ context
+ )
+ expect(
+ [first, second, third].flatMap((result) => result.documents.map((doc) => doc.externalId))
+ ).toEqual(['1', '2', '3'])
+ expect([first.hasMore, second.hasMore, third.hasMore]).toEqual([true, true, false])
+ const requests = fetchMock.mock.calls.map(([input]) => requestUrl(input))
+ expect(requests[0].searchParams.get('cql')).toBe(requests[1].searchParams.get('cql'))
+ expect(requests[1].searchParams.get('cursor')).toBe('provider-page-2')
+ expect(requests[2].searchParams.has('cursor')).toBe(false)
+ expect(requests[2].searchParams.get('cql')).toContain('space in ("SPACE_50","SPACE_51")')
+ expect(context.listingCapped).toBeUndefined()
+ })
+
+ it('marks a limit reached before later batches as incomplete for deletion reconciliation', async () => {
+ fetchMock.mockResolvedValueOnce(page('1'))
+ const context: Record = { cloudId: 'cloud' }
+ const result = await confluenceConnector.listDocuments(
+ 'token',
+ { ...sourceConfig, maxPages: '1' },
+ undefined,
+ context
+ )
+ expect(result.hasMore).toBe(false)
+ expect(result.nextCursor).toBeUndefined()
+ expect(context.listingCapped).toBe(true)
+ })
+
+ it('continues through an empty batch without widening the query', async () => {
+ fetchMock.mockResolvedValueOnce(Response.json({ results: [] })).mockResolvedValueOnce(page('1'))
+ const context: Record = { cloudId: 'cloud' }
+ const first = await confluenceConnector.listDocuments('token', sourceConfig, undefined, context)
+ const last = await confluenceConnector.listDocuments(
+ 'token',
+ sourceConfig,
+ first.nextCursor,
+ context
+ )
+ expect(first.documents).toEqual([])
+ expect(first.hasMore).toBe(true)
+ expect(last.documents.map((doc) => doc.externalId)).toEqual(['1'])
+ expect(last.hasMore).toBe(false)
+ })
+
+ it('rejects an invalid batch continuation before requesting any content', async () => {
+ await expect(
+ confluenceConnector.listDocuments('token', sourceConfig, 'space-batches:{"batch":-1}', {
+ cloudId: 'cloud',
+ })
+ ).rejects.toThrow('Invalid Confluence space continuation')
+ expect(fetchMock).not.toHaveBeenCalled()
+ })
+
+ it('bounds CQL requests for long space keys while retaining filters', async () => {
+ const longKeys = Array.from({ length: 20 }, (_, index) => `SPACE_${index}_${'x'.repeat(240)}`)
+ fetchMock.mockImplementation(async () => Response.json({ results: [] }))
+ const context: Record = { cloudId: 'cloud' }
+ let cursor: string | undefined
+ do {
+ const result = await confluenceConnector.listDocuments(
+ 'token',
+ { ...sourceConfig, spaceKey: longKeys, labelFilter: 'published', contentType: 'all' },
+ cursor,
+ context
+ )
+ cursor = result.nextCursor
+ } while (cursor)
+ const queries = fetchMock.mock.calls.map(([input]) => requestUrl(input))
+ expect(queries.length).toBeGreaterThan(1)
+ for (const url of queries) {
+ expect(url.toString().length).toBeLessThan(2_300)
+ expect(url.searchParams.get('cql')).toContain(
+ 'type in ("page","blogpost") AND label="published"'
+ )
+ }
+ for (const key of longKeys) {
+ expect(
+ queries.filter((url) => url.searchParams.get('cql')?.includes(`"${key}"`))
+ ).toHaveLength(1)
+ }
+ })
+})
diff --git a/apps/sim/connectors/confluence/confluence.ts b/apps/sim/connectors/confluence/confluence.ts
index 1b4bb94e446..eb2f62f068f 100644
--- a/apps/sim/connectors/confluence/confluence.ts
+++ b/apps/sim/connectors/confluence/confluence.ts
@@ -39,6 +39,34 @@ import { getConfluenceCloudId, normalizeConfluenceDomainHost } from '@/tools/con
const logger = createLogger('ConfluenceConnector')
const PERMISSION_VALIDATION_TIMEOUT_MS = 10_000
+const SPACE_BATCH_SIZE = 50
+const SPACE_BATCH_QUERY_LENGTH = 1_800
+const SPACE_BATCH_CURSOR_PREFIX = 'space-batches:'
+
+/** Bounds both space lookups and CQL URLs when a source includes many spaces. */
+function spaceKeyBatches(spaceKeys: string[]): string[][] {
+ const batches: string[][] = []
+ let batch: string[] = []
+ let queryLength = 0
+ for (const key of new Set(spaceKeys)) {
+ const encodedLength = encodeURIComponent(escapeCql(key)).length + 15
+ if (encodedLength > SPACE_BATCH_QUERY_LENGTH) {
+ throw new Error('A Confluence space key is too long. Check the selected spaces.')
+ }
+ if (
+ batch.length === SPACE_BATCH_SIZE ||
+ queryLength + encodedLength > SPACE_BATCH_QUERY_LENGTH
+ ) {
+ batches.push(batch)
+ batch = []
+ queryLength = 0
+ }
+ batch.push(key)
+ queryLength += encodedLength
+ }
+ if (batch.length > 0) batches.push(batch)
+ return batches
+}
/**
* The configured space does not exist for the caller. Confluence answers a
@@ -595,7 +623,7 @@ export const confluenceConnector: ConnectorConfig = {
* `lastModified`.
*/
if (labelFilter.trim() || spaceKeys.length > 1 || lastSyncAt) {
- return listDocumentsViaCql(
+ return listSpaceBatchesViaCql(
cloudId,
accessToken,
domain,
@@ -746,43 +774,64 @@ export const confluenceConnector: ConnectorConfig = {
}
: VALIDATE_RETRY_OPTIONS
const cloudId = await resolveCloudId(accessToken, sourceConfig, syncContext, retryOptions)
- const params = new URLSearchParams()
- for (const key of spaceKeys) params.append('keys', key)
- params.append('limit', String(Math.max(spaceKeys.length, 1)))
- const spaceUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces?${params.toString()}`
- const response = await fetchWithRetry(
- spaceUrl,
- {
- method: 'GET',
- headers: {
- Accept: 'application/json',
- Authorization: `Bearer ${accessToken}`,
- },
- },
- retryOptions
- )
- if (!response.ok) {
- return { valid: false, error: `Failed to validate spaces: ${response.status}` }
- }
- const data = await response.json()
- const results = (data.results as Array> | undefined) ?? []
- const foundKeys = new Set(results.map((r) => String(r.key)))
- const missing = spaceKeys.filter((k) => !foundKeys.has(k))
- if (missing.length > 0) {
- return {
- valid: false,
- error: `Space${missing.length > 1 ? 's' : ''} not found: ${missing.join(', ')}`,
+ let permissionSpaceId: string | undefined
+ for (const batch of spaceKeyBatches(spaceKeys)) {
+ const remainingKeys = new Set(batch)
+ const seenCursors = new Set()
+ let cursor: string | undefined
+ do {
+ const params = new URLSearchParams()
+ for (const key of batch) params.append('keys', key)
+ params.set('limit', String(batch.length))
+ if (cursor) params.set('cursor', cursor)
+ const response = await fetchWithRetry(
+ `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces?${params}`,
+ {
+ method: 'GET',
+ headers: { Accept: 'application/json', Authorization: `Bearer ${accessToken}` },
+ },
+ retryOptions
+ )
+ if (!response.ok) {
+ return { valid: false, error: `Failed to validate spaces: ${response.status}` }
+ }
+ const data = await response.json()
+ if (!Array.isArray(data.results)) {
+ throw new Error('Confluence returned an invalid space list. Try again.')
+ }
+ for (const space of data.results) {
+ if (
+ remainingKeys.delete(space.key) &&
+ !permissionSpaceId &&
+ typeof space.id === 'string'
+ ) {
+ permissionSpaceId = space.id
+ }
+ }
+ if (remainingKeys.size === 0) break
+ const next = data._links?.next
+ cursor = extractCursor(next)
+ if (next && (!cursor || seenCursors.has(cursor) || seenCursors.size >= batch.length)) {
+ throw new Error('Confluence returned an incomplete space list. Try again.')
+ }
+ if (cursor) seenCursors.add(cursor)
+ } while (cursor)
+ if (remainingKeys.size > 0) {
+ const missing = [...remainingKeys]
+ return {
+ valid: false,
+ error: `Space${missing.length > 1 ? 's' : ''} not found: ${missing.join(', ')}`,
+ }
}
}
if (syncContext?.mirrorsSourceAcls === true) {
- const spaceId = results[0]?.id
- if (typeof spaceId !== 'string' || !spaceId) {
+ if (!permissionSpaceId) {
return { valid: false, error: 'Confluence returned a space without an ID. Try again.' }
}
await validateConfluencePermissionAccess({
cloudId,
accessToken,
- spaceId,
+ spaceId: permissionSpaceId,
contentType: (sourceConfig.contentType as string) || 'page',
retryOptions,
})
@@ -1037,6 +1086,86 @@ export function resolveLastModifiedClause(
*/
const CQL_PAGE_SIZE = 50
+/** Walks one bounded space batch at a time without discarding a provider continuation. */
+async function listSpaceBatchesViaCql(
+ cloudId: string,
+ accessToken: string,
+ domain: string,
+ spaceKeys: string[],
+ contentType: string,
+ labelFilter: string,
+ maxPages: number,
+ cursor?: string,
+ syncContext: Record = {},
+ lastSyncAt?: Date
+): Promise {
+ const batches = spaceKeyBatches(spaceKeys)
+ if (batches.length === 1) {
+ return listDocumentsViaCql(
+ cloudId,
+ accessToken,
+ domain,
+ batches[0],
+ contentType,
+ labelFilter,
+ maxPages,
+ cursor,
+ syncContext,
+ lastSyncAt
+ )
+ }
+
+ let batchIndex = 0
+ let providerCursor: string | undefined
+ if (cursor) {
+ const invalidCursor = new Error('Invalid Confluence space continuation. Restart the sync.')
+ if (!cursor.startsWith(SPACE_BATCH_CURSOR_PREFIX)) throw invalidCursor
+ const state: unknown = JSON.parse(cursor.slice(SPACE_BATCH_CURSOR_PREFIX.length))
+ if (
+ typeof state !== 'object' ||
+ state === null ||
+ !('batch' in state) ||
+ typeof state.batch !== 'number' ||
+ !Number.isSafeInteger(state.batch) ||
+ state.batch < 0 ||
+ state.batch >= batches.length ||
+ ('cursor' in state && typeof state.cursor !== 'string')
+ )
+ throw invalidCursor
+ batchIndex = state.batch
+ providerCursor = 'cursor' in state ? (state.cursor as string) : undefined
+ }
+
+ const result = await listDocumentsViaCql(
+ cloudId,
+ accessToken,
+ domain,
+ batches[batchIndex],
+ contentType,
+ labelFilter,
+ maxPages,
+ providerCursor,
+ syncContext,
+ lastSyncAt
+ )
+ const hasMoreBatches = batchIndex + 1 < batches.length
+ if (maxPages > 0 && Number(syncContext.totalDocsFetched) >= maxPages) {
+ if (hasMoreBatches) syncContext.listingCapped = true
+ return { ...result, hasMore: false, nextCursor: undefined }
+ }
+ if (!result.hasMore && !hasMoreBatches) return result
+ return {
+ ...result,
+ hasMore: true,
+ nextCursor:
+ SPACE_BATCH_CURSOR_PREFIX +
+ JSON.stringify({
+ batch: result.hasMore ? batchIndex : batchIndex + 1,
+ ...(result.hasMore ? { cursor: result.nextCursor } : {}),
+ }),
+ }
+}
+
/**
* Lists documents using CQL search via the v1 API (used when label filtering is enabled).
*/
diff --git a/apps/sim/connectors/confluence/meta.ts b/apps/sim/connectors/confluence/meta.ts
index 76076f8e608..b7b3f0782b3 100644
--- a/apps/sim/connectors/confluence/meta.ts
+++ b/apps/sim/connectors/confluence/meta.ts
@@ -64,7 +64,7 @@ export const confluenceConnectorMeta: ConnectorMeta = {
configFields: [
{
id: 'domain',
- title: 'Confluence Domain',
+ title: 'Confluence site',
type: 'short-input',
placeholder: 'yoursite.atlassian.net',
required: true,
@@ -77,6 +77,8 @@ export const confluenceConnectorMeta: ConnectorMeta = {
canonicalParamId: 'spaceKey',
mode: 'basic',
multi: true,
+ allowSelectAll: true,
+ preserveValueOnModeChange: true,
dependsOn: ['domain'],
placeholder: 'Select one or more spaces',
required: true,
diff --git a/apps/sim/connectors/jira/meta.ts b/apps/sim/connectors/jira/meta.ts
index 43f05f39fa3..d99f48cf0d6 100644
--- a/apps/sim/connectors/jira/meta.ts
+++ b/apps/sim/connectors/jira/meta.ts
@@ -16,7 +16,7 @@ export const jiraConnectorMeta: ConnectorMeta = {
configFields: [
{
id: 'domain',
- title: 'Jira Domain',
+ title: 'Jira site',
type: 'short-input',
placeholder: 'yoursite.atlassian.net',
required: true,
@@ -29,6 +29,8 @@ export const jiraConnectorMeta: ConnectorMeta = {
canonicalParamId: 'projectKey',
mode: 'basic',
multi: true,
+ allowSelectAll: true,
+ preserveValueOnModeChange: true,
dependsOn: ['domain'],
placeholder: 'Select one or more projects',
required: true,
diff --git a/apps/sim/connectors/types.ts b/apps/sim/connectors/types.ts
index d6392a19b1f..323325dc743 100644
--- a/apps/sim/connectors/types.ts
+++ b/apps/sim/connectors/types.ts
@@ -301,6 +301,8 @@ export interface ConnectorConfigField {
mode?: 'basic' | 'advanced'
/** Links selector + manual input fields that resolve to the same config key */
canonicalParamId?: string
+ /** Both modes use the same provider identifiers, so switching carries the current selection. */
+ preserveValueOnModeChange?: boolean
/**
* When true, the field accepts multiple values.
@@ -309,6 +311,8 @@ export interface ConnectorConfigField {
* Connector handlers receive `string | string[]` and should normalize via `parseMultiValue`.
*/
multi?: boolean
+ /** Offers explicit bulk selection of the complete, bounded provider list. */
+ allowSelectAll?: boolean
}
/**
diff --git a/apps/sim/ee/credential-groups/components/organization-account-people.tsx b/apps/sim/ee/credential-groups/components/organization-account-people.tsx
index 8bac323aff8..5648b046e6b 100644
--- a/apps/sim/ee/credential-groups/components/organization-account-people.tsx
+++ b/apps/sim/ee/credential-groups/components/organization-account-people.tsx
@@ -16,8 +16,8 @@ import {
RESOURCE_LIST_STACK,
SettingsResourceRow,
} from '@/app/workspace/[workspaceId]/settings/components/settings-resource-row'
-import { EnrollmentConnections } from '@/ee/credential-groups/components/credential-group-enrollment-connections'
import { OrganizationAccountInviteModal } from '@/ee/credential-groups/components/organization-account-invite-modal'
+import { OrganizationPersonConnections } from '@/ee/credential-groups/components/organization-person-connections'
import {
useOrganizationAccountPeople,
useResendOrganizationAccountInvitation,
@@ -37,6 +37,8 @@ interface OrganizationAccountPeopleProps {
actions?: SettingsAction[]
}
enabled?: boolean
+ requestDisabled?: boolean
+ filters?: ReactNode
setupFallback?: ReactNode
}
export function OrganizationAccountPeople({
@@ -45,6 +47,8 @@ export function OrganizationAccountPeople({
panel,
enabled = true,
setupFallback,
+ filters,
+ requestDisabled = false,
}: OrganizationAccountPeopleProps) {
const resend = useResendOrganizationAccountInvitation()
const revoke = useRevokeOrganizationAccountEnrollment()
@@ -69,12 +73,13 @@ export function OrganizationAccountPeople({
text: 'Request connections',
icon: Plus,
variant: 'primary',
- disabled: pending || awaitingSetup,
+ disabled: pending || awaitingSetup || requestDisabled,
onSelect: () => setInviteOpen(true),
},
...(panel?.actions ?? []),
]}
>
+ {filters}
{awaitingSetup ? (
setupFallback
) : (
@@ -110,38 +115,14 @@ export function OrganizationAccountPeople({
icon={ }
iconVariant='custom'
title={person.email}
- description={
- searchConnection && person.status === 'revoked' ? (
- 'Access revoked'
- ) : searchConnection &&
- !person.connections.some(
- (connection) => connection.status === 'active'
- ) ? (
- person.connections.some(
- (connection) => connection.status === 'needs_reauth'
- ) ? (
- 'Reconnect required'
- ) : person.connections.some(
- (connection) => connection.status === 'revoked'
- ) ? (
- 'Disconnected'
- ) : (
- 'Not connected'
- )
- ) : (
-
- )
- }
+ description={ }
trailing={
resend.mutate(
{
diff --git a/apps/sim/ee/credential-groups/components/organization-person-connections.tsx b/apps/sim/ee/credential-groups/components/organization-person-connections.tsx
new file mode 100644
index 00000000000..1654bbf830a
--- /dev/null
+++ b/apps/sim/ee/credential-groups/components/organization-person-connections.tsx
@@ -0,0 +1,86 @@
+import { ChipTag } from '@sim/emcn'
+import type { CredentialGroupEnrollmentDetail } from '@/lib/api/contracts/credential-groups'
+import { getCredentialGroupProviderService } from '@/lib/credential-groups/providers'
+import { resolveCredentialDisplay } from '@/lib/integrations/credential-display'
+
+interface OrganizationPersonConnectionsProps {
+ person: CredentialGroupEnrollmentDetail
+}
+
+const CONNECTION_STATUS_LABELS = {
+ active: 'Connected',
+ needs_reauth: 'Reconnect required',
+ revoked: 'Disconnected',
+} as const
+
+export function OrganizationPersonConnections({ person }: OrganizationPersonConnectionsProps) {
+ if (person.status === 'revoked') return <>Access revoked>
+
+ const connections = person.connections.map((connection) => {
+ const display =
+ connection.provider === 'gitlab'
+ ? resolveCredentialDisplay({
+ type: 'personal_token',
+ providerId: 'gitlab',
+ displayName: 'GitLab',
+ })
+ : undefined
+ const service =
+ connection.provider !== 'gitlab'
+ ? getCredentialGroupProviderService(connection.provider)
+ : undefined
+ return {
+ key: `${connection.provider}:${connection.status}`,
+ name: service?.name ?? display?.detailTitle ?? 'GitLab',
+ icon: service?.icon ?? display?.icon,
+ status: connection.status,
+ count: connection.count,
+ }
+ })
+ const total =
+ connections.reduce(
+ (count, connection) => count + (connection.status === 'active' ? connection.count : 0),
+ 0
+ ) + person.mcpConnections.filter((connection) => connection.status === 'active').length
+
+ const statuses = [...connections, ...person.mcpConnections].map(({ status }) => status)
+
+ return (
+
+
+ {total > 0
+ ? `${total} ${total === 1 ? 'account' : 'accounts'} connected`
+ : statuses.includes('needs_reauth')
+ ? 'Reconnect required'
+ : statuses.includes('revoked')
+ ? 'Disconnected'
+ : person.status === 'delivery_failed'
+ ? 'Connection request failed'
+ : person.expired
+ ? 'Connection request expired'
+ : 'Not connected'}
+
+ {(connections.length > 0 || person.mcpConnections.length > 0) && (
+
+ {connections.map(({ key, name, icon: Icon, status, count }) => (
+
+ {Icon && }
+ {name}
+ {count > 1 ? ` (${count})` : ''}
+ {status !== 'active' && ` · ${CONNECTION_STATUS_LABELS[status]}`}
+
+ ))}
+ {person.mcpConnections.map((connection) => (
+
+ {connection.name} · {CONNECTION_STATUS_LABELS[connection.status]}
+
+ ))}
+
+ )}
+
+ )
+}
diff --git a/apps/sim/ee/credential-groups/components/slack-managed-users-modal.tsx b/apps/sim/ee/credential-groups/components/slack-managed-users-modal.tsx
index aed7460ef51..cd2fd9c6c0b 100644
--- a/apps/sim/ee/credential-groups/components/slack-managed-users-modal.tsx
+++ b/apps/sim/ee/credential-groups/components/slack-managed-users-modal.tsx
@@ -302,7 +302,7 @@ export function SlackManagedUsersModal({
const noBots = !organizationSetup && !isLoading && bots.length === 0
const needsApp = organizationSetup && apps.isSuccess && availableApps.length === 0
- const title = organizationSetup ? 'Connect Slack accounts' : 'Set up Slack'
+ const title = organizationSetup ? 'Set up Slack app' : 'Set up Slack'
const primaryLabel = isLoading
? 'Loading...'
: pending
diff --git a/apps/sim/ee/organization-search-stats/search-params.ts b/apps/sim/ee/organization-search-stats/search-params.ts
new file mode 100644
index 00000000000..0514a128245
--- /dev/null
+++ b/apps/sim/ee/organization-search-stats/search-params.ts
@@ -0,0 +1,22 @@
+import { parseAsString, parseAsStringLiteral } from 'nuqs/server'
+import { SEARCH_STATS_PERIODS, SEARCH_STATS_SURFACES } from '@/lib/knowledge/search/stats'
+
+export const organizationSearchStatsParsers = {
+ period: parseAsStringLiteral(SEARCH_STATS_PERIODS).withDefault('30d'),
+ /** An absent surface includes every Search entry point. */
+ surface: parseAsStringLiteral(SEARCH_STATS_SURFACES),
+ startDate: parseAsString,
+ endDate: parseAsString,
+}
+
+export const organizationSearchStatsUrlOptions = {
+ history: 'replace',
+ shallow: true,
+ clearOnDefault: true,
+ urlKeys: {
+ period: 'stats-period',
+ surface: 'stats-surface',
+ startDate: 'stats-start',
+ endDate: 'stats-end',
+ },
+} as const
diff --git a/apps/sim/hooks/queries/organization-accounts.test.tsx b/apps/sim/hooks/queries/organization-accounts.test.tsx
index b5773eb720d..69f33d1f871 100644
--- a/apps/sim/hooks/queries/organization-accounts.test.tsx
+++ b/apps/sim/hooks/queries/organization-accounts.test.tsx
@@ -13,6 +13,7 @@ import { ApiClientError } from '@/lib/api/client/errors'
import {
disconnectPersonalOrganizationAccountContract,
listOrganizationAccountPeopleContract,
+ revokeOrganizationAccountEnrollmentContract,
updateOrganizationAccountsContract,
} from '@/lib/api/contracts/organization-accounts'
import { resourceScopeKey } from '@/lib/core/resource-scope'
@@ -20,6 +21,7 @@ import {
organizationAccountsKeys,
useDisconnectPersonalOrganizationAccount,
useOrganizationAccountPeople,
+ useRevokeOrganizationAccountEnrollment,
useUpdateOrganizationAccounts,
} from '@/hooks/queries/organization-accounts'
import { slackSearchKeys } from '@/hooks/queries/slack-search'
@@ -207,6 +209,90 @@ describe('organization account setup updates', () => {
)
})
+describe('organization account revocation', () => {
+ it.each([true, false])(
+ 'clears Search content and refreshes affected account projections only on success=%s',
+ async (success) => {
+ vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true)
+ mocks.request.mockReset()
+ const response = Promise.withResolvers()
+ mocks.request.mockReturnValueOnce(response.promise)
+ const client = new QueryClient()
+ const root = createRoot(document.createElement('div'))
+ let mutation: ReturnType
+ function Probe() {
+ mutation = useRevokeOrganizationAccountEnrollment()
+ return null
+ }
+ const orgScope = { kind: 'organization', organizationId: 'org-1' } as const
+ const results = knowledgeKeys.search(resourceScopeKey(orgScope), 'private content')
+ const documentKey = knowledgeKeys.document('kb-1', 'doc-1')
+ const sources = searchSourceKeys.pages(orgScope, { mine: true, search: '' })
+ const accounts = organizationAccountsKeys.detail('org-1')
+ const people = organizationAccountsKeys.peopleList('org-1', '', 'jira-option')
+ const overview = searchSourceKeys.organizationOverview('org-1')
+ const otherAccounts = organizationAccountsKeys.detail('org-2')
+ const otherResults = knowledgeKeys.search(
+ resourceScopeKey({ kind: 'organization', organizationId: 'org-2' }),
+ 'private content'
+ )
+ for (const key of [
+ results,
+ documentKey,
+ sources,
+ accounts,
+ people,
+ overview,
+ otherAccounts,
+ otherResults,
+ ])
+ client.setQueryData(key, { content: 'cached' })
+ try {
+ await act(async () =>
+ root.render(
+
+
+
+ )
+ )
+ let pending: Promise
+ await act(async () => {
+ pending = mutation.mutateAsync({ organizationId: 'org-1', enrollmentId: 'person-1' })
+ })
+ await act(async () =>
+ root.render({null} )
+ )
+ await act(async () => {
+ if (success) {
+ response.resolve({})
+ await pending
+ } else {
+ const rejected = expect(pending).rejects.toThrow('Revocation failed')
+ response.reject(new Error('Revocation failed'))
+ await rejected
+ }
+ })
+ expect(mocks.request).toHaveBeenCalledExactlyOnceWith(
+ revokeOrganizationAccountEnrollmentContract,
+ { params: { id: 'org-1', enrollmentId: 'person-1' } }
+ )
+ for (const key of [results, documentKey, sources]) {
+ if (success) expect(client.getQueryData(key)).toBeUndefined()
+ else expect(client.getQueryData(key)).toEqual({ content: 'cached' })
+ }
+ for (const key of [accounts, people, overview])
+ expect(client.getQueryState(key)?.isInvalidated).toBe(success)
+ expect(client.getQueryState(otherAccounts)?.isInvalidated).toBe(false)
+ expect(client.getQueryData(otherResults)).toEqual({ content: 'cached' })
+ } finally {
+ await act(async () => root.unmount())
+ client.clear()
+ vi.unstubAllGlobals()
+ }
+ }
+ )
+})
+
describe('organization people search pagination', () => {
let root: Root
let container: HTMLDivElement
diff --git a/apps/sim/hooks/queries/organization-accounts.ts b/apps/sim/hooks/queries/organization-accounts.ts
index 65cf2cafef7..7c96859612d 100644
--- a/apps/sim/hooks/queries/organization-accounts.ts
+++ b/apps/sim/hooks/queries/organization-accounts.ts
@@ -312,7 +312,12 @@ export function useRevokeOrganizationAccountEnrollment() {
params: { id: organizationId, enrollmentId },
}),
onSuccess: (_, { organizationId }) =>
- queryClient.invalidateQueries({ queryKey: organizationAccountsKeys.people(organizationId) }),
+ Promise.all([
+ resetOrganizationSearchAccess(queryClient, organizationId),
+ queryClient.invalidateQueries({
+ queryKey: organizationAccountsKeys.detail(organizationId),
+ }),
+ ]),
})
}
export function useAddOrganizationAccountMcpProvider() {
diff --git a/apps/sim/hooks/queries/organization-search-stats.ts b/apps/sim/hooks/queries/organization-search-stats.ts
new file mode 100644
index 00000000000..a11c471103b
--- /dev/null
+++ b/apps/sim/hooks/queries/organization-search-stats.ts
@@ -0,0 +1,28 @@
+'use client'
+
+import { useQuery } from '@tanstack/react-query'
+import { requestJson } from '@/lib/api/client/request'
+import {
+ type OrganizationSearchStatsQuery,
+ readOrganizationSearchStatsContract,
+} from '@/lib/api/contracts/knowledge/search-stats'
+
+export const ORGANIZATION_SEARCH_STATS_STALE_TIME = 60_000
+
+export const organizationSearchStatsKeys = {
+ all: ['organization-search-stats'] as const,
+ summaries: () => [...organizationSearchStatsKeys.all, 'summary'] as const,
+ summary: (query: OrganizationSearchStatsQuery) =>
+ [...organizationSearchStatsKeys.summaries(), query] as const,
+}
+
+export function useOrganizationSearchStats(query: OrganizationSearchStatsQuery) {
+ return useQuery({
+ queryKey: organizationSearchStatsKeys.summary(query),
+ queryFn: async ({ signal }) => {
+ const result = await requestJson(readOrganizationSearchStatsContract, { query, signal })
+ return result.data
+ },
+ staleTime: ORGANIZATION_SEARCH_STATS_STALE_TIME,
+ })
+}
diff --git a/apps/sim/hooks/queries/personal-source-setup.ts b/apps/sim/hooks/queries/personal-source-setup.ts
new file mode 100644
index 00000000000..25a3e0598a6
--- /dev/null
+++ b/apps/sim/hooks/queries/personal-source-setup.ts
@@ -0,0 +1,69 @@
+'use client'
+
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
+import { requestJson } from '@/lib/api/client/request'
+import {
+ listPersonalSourceSetupAccountsContract,
+ type PersonalSourceSetupBody,
+ type PersonalSourceSetupQuery,
+ personalSourceSetupContract,
+} from '@/lib/api/contracts/knowledge/personal-source-setup'
+import { memberConnectorKeys } from '@/hooks/queries/kb/connectors'
+import { organizationAccountsKeys } from '@/hooks/queries/organization-accounts'
+import { personalSearchIntegrationKeys } from '@/hooks/queries/personal-search-integrations'
+import { searchSourceKeys } from '@/hooks/queries/utils/search-source-keys'
+
+export const PERSONAL_SOURCE_SETUP_STALE_TIME = 15_000
+
+export const personalSourceSetupKeys = {
+ all: ['personal-source-setup'] as const,
+ lists: () => [...personalSourceSetupKeys.all, 'list'] as const,
+ list: (query: PersonalSourceSetupQuery) => [...personalSourceSetupKeys.lists(), query] as const,
+}
+
+export function usePersonalSourceSetupAccounts(query: PersonalSourceSetupQuery) {
+ return useQuery({
+ queryKey: personalSourceSetupKeys.list(query),
+ queryFn: async ({ signal }) =>
+ (await requestJson(listPersonalSourceSetupAccountsContract, { query, signal })).data,
+ staleTime: PERSONAL_SOURCE_SETUP_STALE_TIME,
+ refetchInterval: (state) =>
+ query.completionId && !state.state.data?.completedCredentialId ? 1_500 : false,
+ })
+}
+
+export function useAuthorizePersonalSourceSetup() {
+ return useMutation({
+ mutationFn: async (body: Extract) => {
+ const { data } = await requestJson(personalSourceSetupContract, { body })
+ if (data.kind !== 'authorization') throw new Error('Could not start account authorization')
+ return data
+ },
+ })
+}
+
+export function useConnectPersonalSourceSetup() {
+ const client = useQueryClient()
+ return useMutation({
+ mutationFn: async (body: Extract) => {
+ const { data } = await requestJson(personalSourceSetupContract, { body })
+ if (data.kind !== 'connected') throw new Error('Could not connect the source')
+ return data
+ },
+ onSuccess: (_data, body) =>
+ Promise.all([
+ client.invalidateQueries({ queryKey: personalSourceSetupKeys.lists() }),
+ client.invalidateQueries({ queryKey: personalSearchIntegrationKeys.lists() }),
+ client.invalidateQueries({ queryKey: memberConnectorKeys.lists() }),
+ client.invalidateQueries({
+ queryKey: searchSourceKeys.list({
+ kind: 'organization',
+ organizationId: body.organizationId,
+ }),
+ }),
+ client.invalidateQueries({
+ queryKey: organizationAccountsKeys.detail(body.organizationId),
+ }),
+ ]),
+ })
+}
diff --git a/apps/sim/hooks/queries/selectors.test.tsx b/apps/sim/hooks/queries/selectors.test.tsx
index 21ac2bdc2d5..67cf3275117 100644
--- a/apps/sim/hooks/queries/selectors.test.tsx
+++ b/apps/sim/hooks/queries/selectors.test.tsx
@@ -7,15 +7,22 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
-const { mockExecuteSelectorRequest } = vi.hoisted(() => ({
+const { mockExecuteSelectorRequest, mockRequestJson } = vi.hoisted(() => ({
mockExecuteSelectorRequest: vi.fn(),
+ mockRequestJson: vi.fn(),
}))
vi.mock('@/lib/selectors/client/execute-selector', () => ({
executeSelectorRequest: mockExecuteSelectorRequest,
}))
-import { useSelectorOptionDetail, useSelectorOptions } from '@/hooks/queries/selectors'
+vi.mock('@/lib/api/client/request', () => ({ requestJson: mockRequestJson }))
+
+import {
+ type SelectorLoadAllResult,
+ useSelectorOptionDetail,
+ useSelectorOptions,
+} from '@/hooks/queries/selectors'
interface HookHarness {
getResult: () => T
@@ -100,6 +107,276 @@ afterEach(() => {
})
describe('generic selector queries', () => {
+ it('uses the dedicated personal setup contract and isolates it from ordinary browsing', async () => {
+ const personalItems = [{ id: 'PERSONAL', label: 'Personal project' }]
+ mockRequestJson.mockResolvedValue({
+ success: true,
+ data: { kind: 'list', items: personalItems },
+ })
+ mockExecuteSelectorRequest.mockResolvedValue({
+ kind: 'list',
+ items: [{ id: 'ADMIN', label: 'Admin project' }],
+ })
+ const hook = renderHookWithClient(() =>
+ useSelectorOptions('jira.projectKeys', {
+ context: { oauthCredential: 'credential-1', domain: 'example.atlassian.net' },
+ scope: { kind: 'organization', organizationId: 'org-1' },
+ surface: { kind: 'personal-search-setup', organizationId: 'org-1', connectorType: 'jira' },
+ surfaceId: 'projects',
+ })
+ )
+ await waitFor(() => expect(hook.getResult().data).toEqual(personalItems))
+ expect(mockExecuteSelectorRequest).not.toHaveBeenCalled()
+ expect(mockRequestJson).toHaveBeenCalledWith(
+ expect.objectContaining({ path: '/api/knowledge/sim-search/personal-source-setup' }),
+ expect.objectContaining({
+ body: {
+ action: 'options',
+ organizationId: 'org-1',
+ connectorType: 'jira',
+ credentialId: 'credential-1',
+ domain: 'example.atlassian.net',
+ request: { kind: 'list' },
+ },
+ signal: expect.any(AbortSignal),
+ })
+ )
+ hook.rerender(() =>
+ useSelectorOptions('jira.projectKeys', {
+ context: { oauthCredential: 'credential-1', domain: 'example.atlassian.net' },
+ scope: { kind: 'organization', organizationId: 'org-1' },
+ surfaceId: 'projects',
+ })
+ )
+ await waitFor(() =>
+ expect(hook.getResult().data).toEqual([{ id: 'ADMIN', label: 'Admin project' }])
+ )
+ expect(mockExecuteSelectorRequest).toHaveBeenCalledTimes(1)
+ })
+
+ it('hydrates personal setup labels through the same dedicated contract', async () => {
+ mockRequestJson.mockResolvedValue({
+ success: true,
+ data: { kind: 'detail', item: { id: 'ENG', label: 'Engineering' } },
+ })
+ const hook = renderHookWithClient(() =>
+ useSelectorOptionDetail('confluence.spaces', {
+ context: { oauthCredential: 'credential-1', domain: 'example.atlassian.net' },
+ scope: { kind: 'organization', organizationId: 'org-1' },
+ surface: {
+ kind: 'personal-search-setup',
+ organizationId: 'org-1',
+ connectorType: 'confluence',
+ },
+ detailId: 'ENG',
+ })
+ )
+ await waitFor(() => expect(hook.getResult().data?.id).toBe('ENG'))
+ expect(mockRequestJson).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.objectContaining({
+ body: expect.objectContaining({
+ connectorType: 'confluence',
+ request: { kind: 'detail', id: 'ENG' },
+ }),
+ })
+ )
+ expect(mockExecuteSelectorRequest).not.toHaveBeenCalled()
+ })
+
+ it.each(['selector', 'organization'] as const)(
+ 'rejects a mismatched personal setup %s before sending a request',
+ async (mismatch) => {
+ const hook = renderHookWithClient(() =>
+ useSelectorOptions(mismatch === 'selector' ? 'confluence.spaces' : 'jira.projectKeys', {
+ context: { oauthCredential: 'credential-1', domain: 'example.atlassian.net' },
+ scope: {
+ kind: 'organization',
+ organizationId: mismatch === 'organization' ? 'org-2' : 'org-1',
+ },
+ surface: {
+ kind: 'personal-search-setup',
+ organizationId: 'org-1',
+ connectorType: 'jira',
+ },
+ })
+ )
+ await waitFor(() => expect(hook.getResult().error).not.toBeNull())
+ expect(mockRequestJson).not.toHaveBeenCalled()
+ expect(mockExecuteSelectorRequest).not.toHaveBeenCalled()
+ }
+ )
+
+ it.each(['flat', 'paged'] as const)('returns a complete already-loaded %s list', async (mode) => {
+ const items = [{ id: 'ENG', label: 'Engineering' }]
+ mockExecuteSelectorRequest.mockResolvedValue({ kind: 'list', items })
+ const hook = renderHookWithClient(() =>
+ useSelectorOptions(mode === 'paged' ? 'jira.projectKeys' : 'jira.issues', {
+ context: {
+ workspaceId: 'workspace-1',
+ oauthCredential: 'credential-1',
+ domain: 'example.atlassian.net',
+ },
+ })
+ )
+ await waitFor(() => expect(hook.getResult().isSuccess).toBe(true))
+ let result: SelectorLoadAllResult | undefined
+ await act(async () => {
+ result = await hook.getResult().loadAll()
+ })
+ expect(result).toEqual({ status: 'complete', options: items })
+ expect(mockExecuteSelectorRequest).toHaveBeenCalledTimes(1)
+ })
+
+ it.each(['provider', 'options', 'pages'] as const)(
+ 'reports %s truncation without a complete selection',
+ async (kind) => {
+ mockExecuteSelectorRequest.mockImplementation(
+ async ({ request }: { request: { cursor?: string } }) => ({
+ kind: 'list',
+ items:
+ kind === 'options'
+ ? Array.from({ length: 10_001 }, (_, index) => ({
+ id: String(index),
+ label: String(index),
+ }))
+ : [{ id: request.cursor ?? '0', label: 'Project' }],
+ ...(kind === 'pages' ? { nextCursor: String(Number(request.cursor ?? '0') + 1) } : {}),
+ ...(kind === 'provider' ? { truncated: true } : {}),
+ })
+ )
+ const hook = renderHookWithClient(() =>
+ useSelectorOptions('jira.projectKeys', {
+ context: {
+ workspaceId: 'workspace-1',
+ oauthCredential: 'credential-1',
+ domain: 'example.atlassian.net',
+ },
+ })
+ )
+ await waitFor(() => expect(hook.getResult().isSuccess).toBe(true))
+ let result: SelectorLoadAllResult | undefined
+ await act(async () => {
+ result = await hook.getResult().loadAll()
+ })
+ expect(result).toEqual({ status: 'partial' })
+ expect(mockExecuteSelectorRequest).toHaveBeenCalledTimes(kind === 'pages' ? 200 : 1)
+ }
+ )
+
+ it('reports a failed continuation and retries from the first page', async () => {
+ let fail = true
+ mockExecuteSelectorRequest.mockImplementation(
+ async ({ request }: { request: { cursor?: string } }) => {
+ if (request.cursor && fail) throw new Error('Provider is unavailable')
+ return request.cursor
+ ? { kind: 'list', items: [{ id: 'OPS', label: 'Operations' }] }
+ : { kind: 'list', items: [{ id: 'ENG', label: 'Engineering' }], nextCursor: 'next' }
+ }
+ )
+ const hook = renderHookWithClient(() =>
+ useSelectorOptions('jira.projectKeys', {
+ context: {
+ workspaceId: 'workspace-1',
+ oauthCredential: 'credential-1',
+ domain: 'example.atlassian.net',
+ },
+ })
+ )
+ await waitFor(() => expect(hook.getResult().hasMore).toBe(true))
+ let result: SelectorLoadAllResult | undefined
+ await act(async () => {
+ result = await hook.getResult().loadAll()
+ })
+ expect(result).toEqual({ status: 'error' })
+ await waitFor(() => expect(hook.getResult().error).toBeTruthy())
+ fail = false
+ await act(async () => {
+ result = await hook.getResult().loadAll()
+ })
+ expect(result).toEqual({
+ status: 'complete',
+ options: [
+ { id: 'ENG', label: 'Engineering' },
+ { id: 'OPS', label: 'Operations' },
+ ],
+ })
+ expect(mockExecuteSelectorRequest.mock.calls.map(([args]) => args.request.cursor)).toEqual([
+ undefined,
+ 'next',
+ undefined,
+ 'next',
+ ])
+ })
+
+ it.each(['scope', 'surface', 'credential', 'site', 'search', 'disabled', 'unmount'] as const)(
+ 'cancels bulk selection when its %s changes',
+ async (change) => {
+ let resolvePage!: (result: {
+ kind: 'list'
+ items: { id: string; label: string }[]
+ nextCursor?: string
+ }) => void
+ const pending = new Promise<{
+ kind: 'list'
+ items: { id: string; label: string }[]
+ nextCursor?: string
+ }>((resolve) => {
+ resolvePage = resolve
+ })
+ mockExecuteSelectorRequest.mockImplementation(
+ ({ request }: { request: { cursor?: string } }) =>
+ request.cursor
+ ? pending
+ : Promise.resolve({
+ kind: 'list',
+ items: [{ id: 'ENG', label: 'Engineering' }],
+ nextCursor: 'next',
+ })
+ )
+ let workspaceId = 'workspace-1'
+ let surfaceId = 'field-1'
+ let credentialId = 'credential-1'
+ let domain = 'example.atlassian.net'
+ let search = ''
+ let enabled = true
+ const useHook = () =>
+ useSelectorOptions('jira.projectKeys', {
+ context: { workspaceId, oauthCredential: credentialId, domain },
+ surfaceId,
+ search,
+ enabled,
+ })
+ const hook = renderHookWithClient(useHook)
+ await waitFor(() => expect(hook.getResult().hasMore).toBe(true))
+ let result!: Promise
+ act(() => {
+ result = hook.getResult().loadAll()
+ })
+ await waitFor(() => expect(mockExecuteSelectorRequest).toHaveBeenCalledTimes(2))
+ if (change === 'scope') workspaceId = 'workspace-2'
+ if (change === 'surface') surfaceId = 'field-2'
+ if (change === 'credential') credentialId = 'credential-2'
+ if (change === 'site') domain = 'another.atlassian.net'
+ if (change === 'search') search = 'Operations'
+ if (change === 'disabled') enabled = false
+ if (change === 'unmount') hook.unmount()
+ else hook.rerender(useHook)
+ await act(async () => {
+ resolvePage({
+ kind: 'list',
+ items: [{ id: 'OPS', label: 'Operations' }],
+ nextCursor: 'unused',
+ })
+ expect(await result).toEqual({ status: 'cancelled' })
+ })
+ expect(
+ mockExecuteSelectorRequest.mock.calls.filter(([args]) => args.request.cursor)
+ ).toHaveLength(1)
+ if (change !== 'unmount') expect(hook.getResult().isLoadingAll).toBe(false)
+ }
+ )
+
it('transports supported search and keeps context and request plaintext out of query keys', async () => {
const credentialReference = '{{SHARED_GOOGLE_CREDENTIAL}}'
const search = 'private search phrase'
@@ -365,7 +642,10 @@ describe('generic selector queries', () => {
await waitFor(() => expect(hook.getResult().hasMore).toBe(true))
expect(mockExecuteSelectorRequest).toHaveBeenCalledTimes(1)
- act(() => hook.getResult().loadAll())
+ let result: SelectorLoadAllResult | undefined
+ await act(async () => {
+ result = await hook.getResult().loadAll()
+ })
await waitFor(() => expect(hook.getResult().isLoadingAll).toBe(false))
expect(mockExecuteSelectorRequest).toHaveBeenCalledTimes(3)
@@ -375,6 +655,7 @@ describe('generic selector queries', () => {
{ id: 'workspace-2', label: 'Workspace 2' },
])
expect(hook.getResult()).toMatchObject({ hasMore: false, truncated: false })
+ expect(result).toEqual({ status: 'complete', options: hook.getResult().data })
})
it('refreshes from the first page before retrying a failed continuation cursor', async () => {
diff --git a/apps/sim/hooks/queries/selectors.ts b/apps/sim/hooks/queries/selectors.ts
index c30671f41b5..a92dcf537c1 100644
--- a/apps/sim/hooks/queries/selectors.ts
+++ b/apps/sim/hooks/queries/selectors.ts
@@ -2,7 +2,12 @@
import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'
import { useInfiniteQuery, useQueries, useQuery } from '@tanstack/react-query'
-import { executeSelectorRequest } from '@/lib/selectors/client/execute-selector'
+import { requestJson } from '@/lib/api/client/request'
+import { personalSourceSetupContract } from '@/lib/api/contracts/knowledge/personal-source-setup'
+import {
+ type ExecuteSelectorClientInput,
+ executeSelectorRequest,
+} from '@/lib/selectors/client/execute-selector'
import { projectSelectorContext } from '@/lib/selectors/context'
import { MAX_SELECTOR_OPTIONS, MAX_SELECTOR_PAGES } from '@/lib/selectors/limits'
import {
@@ -16,12 +21,12 @@ import type {
SelectorOption,
SelectorPage,
SelectorScope,
+ SelectorSurface,
} from '@/lib/selectors/types'
import { selectorKeys } from '@/hooks/queries/utils/selector-keys'
type SelectorListResult = Extract
-const EMPTY_PAGE: SelectorListResult = { kind: 'list', items: [] }
let nextOpaqueRevision = 1
export type SelectorClientContext = SelectorContext & {
@@ -35,6 +40,38 @@ interface SelectorHookArgs {
search?: string
enabled?: boolean
surfaceId?: string
+ surface?: SelectorSurface
+}
+
+async function executeForSurface(
+ input: ExecuteSelectorClientInput,
+ surface?: SelectorSurface
+): Promise {
+ if (!surface) return executeSelectorRequest(input)
+ const expectedKey = surface.connectorType === 'jira' ? 'jira.projectKeys' : 'confluence.spaces'
+ if (
+ input.selectorKey !== expectedKey ||
+ input.scope?.kind !== 'organization' ||
+ input.scope.organizationId !== surface.organizationId ||
+ !input.context.oauthCredential ||
+ !input.context.domain
+ )
+ throw new Error('This selector is not available during personal source setup')
+ const result = await requestJson(personalSourceSetupContract, {
+ body: {
+ action: 'options',
+ organizationId: surface.organizationId,
+ connectorType: surface.connectorType,
+ credentialId: input.context.oauthCredential,
+ domain: input.context.domain,
+ request: input.request,
+ },
+ signal: input.signal,
+ })
+ if (result.data.kind !== 'list' && result.data.kind !== 'detail') {
+ throw new Error('Personal source setup returned an unexpected selector result')
+ }
+ return result.data
}
export interface SelectorOptionsResult {
@@ -48,10 +85,14 @@ export interface SelectorOptionsResult {
error: Error | null
isSuccess: boolean
loadMore(): void
- loadAll(): void
+ loadAll(): Promise
refetch(): void
}
+export type SelectorLoadAllResult =
+ | { status: 'complete'; options: SelectorOption[] }
+ | { status: 'partial' | 'error' | 'cancelled' }
+
interface CollectedSelectorOptions {
options: SelectorOption[]
overflowed: boolean
@@ -140,7 +181,13 @@ function usePreparedSelector(
const context = projectSelectorContext(key, args.context)
const scope = selectorScopeFromContext(args.context, args.scope)
const contextValues = manifest.context.allowed.map((field) => context[field])
- const revision = useOpaqueRevision([...contextValues, ...requestValues])
+ const revision = useOpaqueRevision([
+ ...contextValues,
+ ...requestValues,
+ args.surface?.kind,
+ args.surface?.organizationId,
+ args.surface?.connectorType,
+ ])
const ready =
args.enabled !== false &&
isSelectorReady(key, context) &&
@@ -152,6 +199,7 @@ function usePreparedSelector(
revision,
ready,
surfaceId: args.surfaceId ?? generatedSurfaceId,
+ surface: args.surface,
}
}
@@ -174,16 +222,19 @@ export function useSelectorOptions(
// rq-lint-allow: context and search are represented by an opaque privacy revision.
queryKey: baseKey,
queryFn: async ({ signal }) => {
- const result = await executeSelectorRequest({
- selectorKey: key,
- scope: prepared.scope,
- context: prepared.context,
- request: {
- kind: 'list',
- ...(effectiveSearch !== undefined ? { search: effectiveSearch } : {}),
+ const result = await executeForSurface(
+ {
+ selectorKey: key,
+ scope: prepared.scope,
+ context: prepared.context,
+ request: {
+ kind: 'list',
+ ...(effectiveSearch !== undefined ? { search: effectiveSearch } : {}),
+ },
+ signal,
},
- signal,
- })
+ prepared.surface
+ )
if (result.kind !== 'list') throw new Error('Selector returned an unexpected detail result')
return result
},
@@ -196,18 +247,21 @@ export function useSelectorOptions(
// rq-lint-allow: context and search are represented by an opaque privacy revision.
queryKey: [...baseKey, 'paged'],
queryFn: async ({ pageParam, signal }) => {
- const result = await executeSelectorRequest({
- selectorKey: key,
- scope: prepared.scope,
- context: prepared.context,
- request: {
- kind: 'list',
- ...(effectiveSearch !== undefined ? { search: effectiveSearch } : {}),
- ...(typeof pageParam === 'string' ? { cursor: pageParam } : {}),
+ const result = await executeForSurface(
+ {
+ selectorKey: key,
+ scope: prepared.scope,
+ context: prepared.context,
+ request: {
+ kind: 'list',
+ ...(effectiveSearch !== undefined ? { search: effectiveSearch } : {}),
+ ...(typeof pageParam === 'string' ? { cursor: pageParam } : {}),
+ },
+ signal,
},
- signal,
- })
- if (result.kind !== 'list') return EMPTY_PAGE
+ prepared.surface
+ )
+ if (result.kind !== 'list') throw new Error('Selector returned an unexpected detail result')
return result
},
getNextPageParam: (last) => last.nextCursor,
@@ -231,73 +285,88 @@ export function useSelectorOptions(
const loadGenerationRef = useRef(0)
const pageFetchInFlightRef = useRef(false)
const [isLoadingAll, setIsLoadingAll] = useState(false)
+ const requestIdentity = JSON.stringify(baseKey)
useEffect(() => {
loadGenerationRef.current += 1
pageFetchInFlightRef.current = false
setIsLoadingAll(false)
- }, [key, prepared.revision])
+ return () => {
+ loadGenerationRef.current += 1
+ pageFetchInFlightRef.current = false
+ }
+ }, [requestIdentity, prepared.ready])
const loadMore = useCallback(() => {
if (!canLoadMore || pageFetchInFlightRef.current) return
+ const generation = ++loadGenerationRef.current
pageFetchInFlightRef.current = true
void (async () => {
if (pagedQuery.isFetchNextPageError) {
const refreshed = await pagedQuery.refetch()
+ if (loadGenerationRef.current !== generation) return
const refreshedPages = refreshed.data?.pages
const refreshedLastPage = refreshedPages?.[refreshedPages.length - 1]
if (refreshed.isError || !refreshedLastPage?.nextCursor) return
}
await pagedQuery.fetchNextPage()
})().finally(() => {
- pageFetchInFlightRef.current = false
+ if (loadGenerationRef.current === generation) pageFetchInFlightRef.current = false
})
}, [canLoadMore, pagedQuery.fetchNextPage, pagedQuery.isFetchNextPageError, pagedQuery.refetch])
- const loadAll = useCallback(() => {
- if (!canLoadMore || pageFetchInFlightRef.current) return
- const generation = loadGenerationRef.current + 1
- loadGenerationRef.current = generation
+ const loadAll = useCallback(async (): Promise => {
+ if (!prepared.ready || pageFetchInFlightRef.current || pagedQuery.isFetching) {
+ return { status: 'cancelled' }
+ }
+ const generation = ++loadGenerationRef.current
pageFetchInFlightRef.current = true
setIsLoadingAll(true)
- void (async () => {
- let hasNextPage = Boolean(pagedQuery.hasNextPage)
- let pages = pagedQuery.data?.pages
- try {
- if (pagedQuery.isFetchNextPageError) {
- const refreshed = await pagedQuery.refetch()
- if (loadGenerationRef.current !== generation || refreshed.isError) return
- pages = refreshed.data?.pages
- const refreshedLastPage = pages?.[pages.length - 1]
- hasNextPage = Boolean(refreshedLastPage?.nextCursor)
- }
- while (hasNextPage) {
- if (
- (pages?.length ?? 0) >= MAX_SELECTOR_PAGES ||
- collectSelectorOptions(pages).options.length >= MAX_SELECTOR_OPTIONS
- ) {
- break
- }
- const result = await pagedQuery.fetchNextPage()
- if (loadGenerationRef.current !== generation) return
- if (result.isError) break
- hasNextPage = Boolean(result.hasNextPage)
- pages = result.data?.pages
- }
- } finally {
- if (loadGenerationRef.current === generation) {
- pageFetchInFlightRef.current = false
- setIsLoadingAll(false)
- }
+ let hasNextPage = Boolean(pagedQuery.hasNextPage)
+ let pages = pagedQuery.data?.pages
+ try {
+ if (pagedQuery.isError || !pages) {
+ const refreshed = await pagedQuery.refetch()
+ if (loadGenerationRef.current !== generation) return { status: 'cancelled' }
+ if (refreshed.isError) return { status: 'error' }
+ pages = refreshed.data?.pages
+ const refreshedLastPage = pages?.[pages.length - 1]
+ hasNextPage = Boolean(refreshedLastPage?.nextCursor)
}
- })()
+ while (hasNextPage) {
+ const collected = collectSelectorOptions(pages)
+ if (
+ (pages?.length ?? 0) >= MAX_SELECTOR_PAGES ||
+ collected.options.length >= MAX_SELECTOR_OPTIONS ||
+ pages?.some((page) => page.truncated)
+ )
+ return { status: 'partial' }
+ const result = await pagedQuery.fetchNextPage()
+ if (loadGenerationRef.current !== generation) return { status: 'cancelled' }
+ if (result.isError) return { status: 'error' }
+ hasNextPage = Boolean(result.hasNextPage)
+ pages = result.data?.pages
+ }
+ const collected = collectSelectorOptions(pages)
+ return collected.overflowed || pages?.some((page) => page.truncated)
+ ? { status: 'partial' }
+ : { status: 'complete', options: collected.options }
+ } catch {
+ return { status: loadGenerationRef.current === generation ? 'error' : 'cancelled' }
+ } finally {
+ if (loadGenerationRef.current === generation) {
+ pageFetchInFlightRef.current = false
+ setIsLoadingAll(false)
+ }
+ }
}, [
- canLoadMore,
+ prepared.ready,
pagedQuery.data?.pages,
pagedQuery.fetchNextPage,
pagedQuery.hasNextPage,
- pagedQuery.isFetchNextPageError,
+ pagedQuery.isError,
+ pagedQuery.isFetching,
pagedQuery.refetch,
])
@@ -337,7 +406,14 @@ export function useSelectorOptions(
error: (flatQuery.error as Error | null) ?? null,
isSuccess: flatQuery.isSuccess,
loadMore: () => undefined,
- loadAll: () => undefined,
+ loadAll: async () => {
+ if (!prepared.ready || flatQuery.isFetching) return { status: 'cancelled' }
+ if (flatQuery.isError || !flatQuery.data) return { status: 'error' }
+ const collected = collectSelectorOptions([flatQuery.data])
+ return flatQuery.data.truncated || collected.overflowed
+ ? { status: 'partial' }
+ : { status: 'complete', options: collected.options }
+ },
refetch: () => {
if (!prepared.ready) return
void flatQuery.refetch()
@@ -365,13 +441,16 @@ export function useSelectorOptionDetail(
prepared.revision
),
queryFn: async ({ signal }) => {
- const result = await executeSelectorRequest({
- selectorKey: key,
- scope: prepared.scope,
- context: prepared.context,
- request: { kind: 'detail', id: args.detailId! },
- signal,
- })
+ const result = await executeForSurface(
+ {
+ selectorKey: key,
+ scope: prepared.scope,
+ context: prepared.context,
+ request: { kind: 'detail', id: args.detailId! },
+ signal,
+ },
+ prepared.surface
+ )
if (result.kind !== 'detail') throw new Error('Selector returned an unexpected list result')
return result.item
},
@@ -399,13 +478,16 @@ export function useSelectorOptionDetails(
ordinal
),
queryFn: async ({ signal }: { signal: AbortSignal }) => {
- const result = await executeSelectorRequest({
- selectorKey: key,
- scope: prepared.scope,
- context: prepared.context,
- request: { kind: 'detail', id: detailId },
- signal,
- })
+ const result = await executeForSurface(
+ {
+ selectorKey: key,
+ scope: prepared.scope,
+ context: prepared.context,
+ request: { kind: 'detail', id: detailId },
+ signal,
+ },
+ prepared.surface
+ )
if (result.kind !== 'detail') throw new Error('Selector returned an unexpected list result')
return result.item
},
diff --git a/apps/sim/hooks/use-personal-source-account.test.tsx b/apps/sim/hooks/use-personal-source-account.test.tsx
new file mode 100644
index 00000000000..cccfe0e977f
--- /dev/null
+++ b/apps/sim/hooks/use-personal-source-account.test.tsx
@@ -0,0 +1,153 @@
+/**
+ * @vitest-environment jsdom
+ */
+import { act } from 'react'
+import { createRoot, type Root } from 'react-dom/client'
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+
+const mocks = vi.hoisted(() => ({
+ authorize: vi.fn(),
+ refetch: vi.fn(),
+ error: vi.fn(),
+ completed: null as string | null,
+ cache: vi.fn(),
+ connected: vi.fn(),
+}))
+vi.mock('@tanstack/react-query', () => ({ useQueryClient: () => ({ setQueryData: mocks.cache }) }))
+vi.mock('@sim/emcn', () => ({ toast: { error: mocks.error } }))
+vi.mock('@/hooks/queries/personal-source-setup', () => ({
+ personalSourceSetupKeys: { list: (query: unknown) => ['personal-source-setup', query] },
+ useAuthorizePersonalSourceSetup: () => ({ mutateAsync: mocks.authorize, isPending: false }),
+ usePersonalSourceSetupAccounts: () => ({
+ data: { accounts: [], completedCredentialId: mocks.completed },
+ refetch: mocks.refetch,
+ }),
+}))
+
+import { usePersonalSourceAccount } from '@/hooks/use-personal-source-account'
+
+describe('personal source account authorization', () => {
+ let root: Root
+ let container: HTMLDivElement
+ let current: ReturnType
+ let channels: Array<{
+ onmessage: ((event: MessageEvent) => void) | null
+ close: ReturnType
+ }>
+ let tab: {
+ opener: unknown
+ location: { href: string }
+ focus: ReturnType
+ close: ReturnType
+ }
+ function Probe() {
+ current = usePersonalSourceAccount({
+ organizationId: 'org-1',
+ connectorType: 'jira',
+ onConnected: mocks.connected,
+ })
+ return null
+ }
+ beforeEach(() => {
+ vi.clearAllMocks()
+ vi.useFakeTimers()
+ mocks.completed = null
+ mocks.authorize.mockResolvedValue({
+ kind: 'authorization',
+ url: 'https://auth.atlassian.com/authorize',
+ })
+ channels = []
+ vi.stubGlobal(
+ 'BroadcastChannel',
+ class {
+ onmessage = null
+ close = vi.fn()
+ constructor() {
+ channels.push(this)
+ }
+ }
+ )
+ tab = { opener: {}, location: { href: 'about:blank' }, focus: vi.fn(), close: vi.fn() }
+ vi.spyOn(window, 'open').mockReturnValue(tab as unknown as Window)
+ ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
+ container = document.createElement('div')
+ document.body.appendChild(container)
+ root = createRoot(container)
+ act(() => root.render( ))
+ })
+ afterEach(() => {
+ act(() => root.unmount())
+ container.remove()
+ vi.useRealTimers()
+ vi.restoreAllMocks()
+ vi.unstubAllGlobals()
+ })
+ it('authorizes before a source exists and requires server confirmation of completion', async () => {
+ await act(async () => current.connect())
+ expect(tab.opener).toBeNull()
+ expect(tab.location.href).toBe('https://auth.atlassian.com/authorize')
+ expect(mocks.authorize).toHaveBeenCalledWith(
+ expect.objectContaining({
+ action: 'authorize',
+ organizationId: 'org-1',
+ connectorType: 'jira',
+ oauthCompletionId: expect.any(String),
+ })
+ )
+ expect(current.pending).toBe(true)
+ act(() => channels[0].onmessage?.({ data: 'connected' } as MessageEvent))
+ expect(mocks.refetch).toHaveBeenCalledOnce()
+ expect(current.pending).toBe(true)
+ expect(tab.close).not.toHaveBeenCalled()
+ mocks.completed = 'my-account'
+ act(() => root.render( ))
+ expect(current.pending).toBe(false)
+ expect(tab.close).toHaveBeenCalledOnce()
+ expect(mocks.connected).toHaveBeenCalledWith('my-account')
+ mocks.completed = null
+ act(() => root.render( ))
+ expect(current.pending).toBe(false)
+ })
+ it('shows an account mismatch as a toast and allows a fresh attempt', async () => {
+ await act(async () => current.connect())
+ act(() => channels[0].onmessage?.({ data: 'account_mismatch' } as MessageEvent))
+ expect(mocks.error).toHaveBeenCalledWith('Choose the account matching your Sim email address.')
+ expect(current.pending).toBe(false)
+ await act(async () => current.connect())
+ expect(mocks.authorize).toHaveBeenCalledTimes(2)
+ })
+ it('ignores a late authorization response after cancellation', async () => {
+ let resolve!: (value: { kind: string; url: string }) => void
+ mocks.authorize.mockReturnValue(
+ new Promise((done) => {
+ resolve = done
+ })
+ )
+ let connecting!: Promise
+ act(() => {
+ connecting = current.connect()
+ })
+ act(() => current.cancel())
+ await act(async () => {
+ resolve({ kind: 'authorization', url: 'https://auth.atlassian.com/authorize' })
+ await connecting
+ })
+ expect(tab.location.href).toBe('about:blank')
+ expect(current.pending).toBe(false)
+ })
+ it('times out pending authorization and closes its channel on unmount', async () => {
+ await act(async () => current.connect())
+ act(() => vi.advanceTimersByTime(10 * 60_000))
+ expect(current.pending).toBe(false)
+ expect(mocks.error).toHaveBeenCalledWith(
+ 'This connection attempt expired. Try connecting your account again.'
+ )
+ expect(channels[0].close).toHaveBeenCalledOnce()
+ })
+ it('reports popup blocking without starting enrollment', async () => {
+ vi.mocked(window.open).mockReturnValue(null)
+ await act(async () => current.connect())
+ expect(mocks.authorize).not.toHaveBeenCalled()
+ expect(mocks.error).toHaveBeenCalledWith('Allow pop-ups for this site to connect your account.')
+ })
+})
diff --git a/apps/sim/hooks/use-personal-source-account.ts b/apps/sim/hooks/use-personal-source-account.ts
new file mode 100644
index 00000000000..3ac8447127d
--- /dev/null
+++ b/apps/sim/hooks/use-personal-source-account.ts
@@ -0,0 +1,135 @@
+'use client'
+
+import { useCallback, useEffect, useRef, useState } from 'react'
+import { toast } from '@sim/emcn'
+import { getErrorMessage } from '@sim/utils/errors'
+import { generateId } from '@sim/utils/id'
+import { useQueryClient } from '@tanstack/react-query'
+import type { PersonalSourceSetupQuery } from '@/lib/api/contracts/knowledge/personal-source-setup'
+import {
+ CREDENTIAL_GROUP_OAUTH_FAILURE_MESSAGES,
+ credentialGroupOAuthCompletionChannel,
+ isCredentialGroupOAuthFailure,
+} from '@/lib/credential-groups/oauth-completion'
+import {
+ personalSourceSetupKeys,
+ useAuthorizePersonalSourceSetup,
+ usePersonalSourceSetupAccounts,
+} from '@/hooks/queries/personal-source-setup'
+
+interface PersonalSourceAccountProps {
+ organizationId: string
+ connectorType: PersonalSourceSetupQuery['connectorType']
+ onConnected?: (credentialId: string) => void
+}
+
+/** A popup message only refreshes the account inventory; the server proves completion. */
+export function usePersonalSourceAccount({
+ organizationId,
+ connectorType,
+ onConnected,
+}: PersonalSourceAccountProps) {
+ const [completionId, setCompletionId] = useState()
+ const attempt = useRef<{ id: string; tab: Window } | null>(null)
+ const accounts = usePersonalSourceSetupAccounts({ organizationId, connectorType, completionId })
+ const { mutateAsync: authorize } = useAuthorizePersonalSourceSetup()
+ const queryClient = useQueryClient()
+ const onConnectedRef = useRef(onConnected)
+ const completed = accounts.data?.completedCredentialId
+ const { refetch } = accounts
+
+ useEffect(() => {
+ onConnectedRef.current = onConnected
+ }, [onConnected])
+
+ useEffect(() => {
+ return () => {
+ attempt.current?.tab.close()
+ attempt.current = null
+ }
+ }, [])
+
+ useEffect(() => {
+ if (!completionId || completed) return
+ const fail = (message: string) => {
+ if (attempt.current?.id !== completionId) return
+ attempt.current.tab.close()
+ attempt.current = null
+ setCompletionId(undefined)
+ toast.error(message)
+ }
+ const channel = new BroadcastChannel(credentialGroupOAuthCompletionChannel(completionId))
+ channel.onmessage = ({ data }: MessageEvent) => {
+ if (isCredentialGroupOAuthFailure(data)) fail(CREDENTIAL_GROUP_OAUTH_FAILURE_MESSAGES[data])
+ else if (data === 'connected') void refetch()
+ }
+ const timer = window.setTimeout(
+ () => fail(CREDENTIAL_GROUP_OAUTH_FAILURE_MESSAGES.expired),
+ 10 * 60_000
+ )
+ return () => {
+ channel.close()
+ window.clearTimeout(timer)
+ }
+ }, [completionId, completed, refetch])
+
+ useEffect(() => {
+ if (completed && attempt.current && attempt.current.id === completionId) {
+ attempt.current.tab.close()
+ attempt.current = null
+ setCompletionId(undefined)
+ queryClient.setQueryData(personalSourceSetupKeys.list({ organizationId, connectorType }), {
+ ...accounts.data,
+ completedCredentialId: null,
+ })
+ onConnectedRef.current?.(completed)
+ }
+ }, [completed, completionId, accounts.data, queryClient, organizationId, connectorType])
+
+ const connect = useCallback(async () => {
+ if (attempt.current) {
+ attempt.current.tab.focus()
+ return
+ }
+ const tab = window.open('about:blank', '_blank', 'width=600,height=700')
+ if (!tab) {
+ toast.error('Allow pop-ups for this site to connect your account.')
+ return
+ }
+ tab.opener = null
+ const id = generateId()
+ attempt.current = { id, tab }
+ setCompletionId(id)
+ try {
+ const result = await authorize({
+ action: 'authorize',
+ organizationId,
+ connectorType,
+ oauthCompletionId: id,
+ })
+ if (attempt.current?.id !== id) return
+ const url = new URL(result.url)
+ if (
+ url.protocol !== 'https:' &&
+ !(url.protocol === 'http:' && url.origin === window.location.origin)
+ ) {
+ throw new Error('The provider authorization URL is invalid')
+ }
+ tab.location.href = url.href
+ } catch (error) {
+ if (attempt.current?.id !== id) return
+ tab.close()
+ attempt.current = null
+ setCompletionId(undefined)
+ toast.error(getErrorMessage(error, 'Could not connect your account'))
+ }
+ }, [authorize, organizationId, connectorType])
+
+ const cancel = useCallback(() => {
+ attempt.current?.tab.close()
+ attempt.current = null
+ setCompletionId(undefined)
+ }, [])
+
+ return { accounts, connect, cancel, pending: Boolean(completionId) }
+}
diff --git a/apps/sim/hooks/use-search-integration-connection.test.tsx b/apps/sim/hooks/use-search-integration-connection.test.tsx
index 421180cab62..504e94bd0e9 100644
--- a/apps/sim/hooks/use-search-integration-connection.test.tsx
+++ b/apps/sim/hooks/use-search-integration-connection.test.tsx
@@ -134,6 +134,20 @@ afterEach(() => {
})
describe('Search connection card lifecycle', () => {
+ it('confirms account-first setup against inventory and reflects later revocation', () => {
+ m.requestedTarget = { type: 'link', provider: 'jira', connectorType: 'jira' }
+ render()
+ act(() => connection().completeSetup({ connectorId: 'new-source', credentialId: 'mine' }))
+ expect(connection().connectorId).toBe('new-source')
+ expect(connection().connected).toBe(false)
+ expect(m.mutate).not.toHaveBeenCalled()
+ m.accounts = [{ credentialId: 'mine', status: 'connected' }]
+ render()
+ expect(connection().connected).toBe(true)
+ m.accounts = [{ credentialId: 'mine', status: 'reconnect_needed' }]
+ render()
+ expect(connection().connected).toBe(false)
+ })
it('starts OAuth only on click and completes only when its receipt and current account agree', async () => {
render()
expect(m.mutate).not.toHaveBeenCalled()
diff --git a/apps/sim/hooks/use-search-integration-connection.ts b/apps/sim/hooks/use-search-integration-connection.ts
index 96c455ac16f..3f0cd7873cd 100644
--- a/apps/sim/hooks/use-search-integration-connection.ts
+++ b/apps/sim/hooks/use-search-integration-connection.ts
@@ -213,7 +213,21 @@ export function useSearchIntegrationConnection({
error: 'Connection canceled. You can try again.',
})
}, [attempt, key])
+ const completeSetup = useCallback(
+ (result: { connectorId: string; credentialId: string }) => {
+ writeSearchConnectionAttempt(key, {
+ completionId: generateId(),
+ requestedAt: Date.now(),
+ connectorId: result.connectorId,
+ credentialId: result.credentialId,
+ status: 'connected',
+ error: null,
+ })
+ },
+ [key]
+ )
return {
+ completeSetup,
connect,
cancel,
inventoryError: query.error?.message,
diff --git a/apps/sim/lib/api/contracts/knowledge/personal-source-setup.test.ts b/apps/sim/lib/api/contracts/knowledge/personal-source-setup.test.ts
new file mode 100644
index 00000000000..58e8fa013b5
--- /dev/null
+++ b/apps/sim/lib/api/contracts/knowledge/personal-source-setup.test.ts
@@ -0,0 +1,47 @@
+/** @vitest-environment node */
+import { describe, expect, it } from 'vitest'
+import { personalSourceSetupBodySchema } from '@/lib/api/contracts/knowledge/personal-source-setup'
+import { executeSelectorBodySchema } from '@/lib/api/contracts/selectors/execute'
+
+const source = {
+ action: 'connect',
+ organizationId: 'organization-1',
+ connectorType: 'jira',
+ credentialId: 'account-1',
+ domain: 'example.atlassian.net',
+ keys: ['PROJECT'],
+}
+
+describe('personal Search setup contracts', () => {
+ it('accepts a bounded bulk selection larger than the legacy joined-string limit', () => {
+ const keys = Array.from({ length: 1000 }, (_, index) => `PROJECT${index}`)
+ expect(personalSourceSetupBodySchema.parse({ ...source, keys })).toMatchObject({ keys })
+ })
+ it.each([[], Array.from({ length: 1001 }, (_, index) => `P${index}`), [''], ['x'.repeat(256)]])(
+ 'rejects empty, oversized, or unbounded selections %#',
+ (keys) => {
+ expect(personalSourceSetupBodySchema.safeParse({ ...source, keys }).success).toBe(false)
+ }
+ )
+ it('rejects arbitrary credential kinds, scope overrides and provider keys', () => {
+ for (const extra of [
+ { workspaceId: 'workspace-1' },
+ { selectorKey: 'jira.issues' },
+ { accessMode: 'admin' },
+ { connectorType: 'slack' },
+ ]) {
+ expect(personalSourceSetupBodySchema.safeParse({ ...source, ...extra }).success).toBe(false)
+ }
+ })
+ it('does not expose the trusted personal browsing marker through the generic selector API', () => {
+ expect(
+ executeSelectorBodySchema.safeParse({
+ selectorKey: 'jira.projectKeys',
+ scope: { kind: 'organization', organizationId: 'organization-1' },
+ context: { oauthCredential: 'account-1', domain: 'example.atlassian.net' },
+ request: { kind: 'list' },
+ personalSearchSetup: 'jira',
+ }).success
+ ).toBe(false)
+ })
+})
diff --git a/apps/sim/lib/api/contracts/knowledge/personal-source-setup.ts b/apps/sim/lib/api/contracts/knowledge/personal-source-setup.ts
new file mode 100644
index 00000000000..c3e64253078
--- /dev/null
+++ b/apps/sim/lib/api/contracts/knowledge/personal-source-setup.ts
@@ -0,0 +1,86 @@
+import { z } from 'zod'
+import { defineRouteContract } from '@/lib/api/contracts'
+import { successResponseSchema } from '@/lib/api/contracts/knowledge/shared'
+import { organizationIdSchema } from '@/lib/api/contracts/primitives'
+import {
+ executeSelectorResponseSchema,
+ selectorRequestSchema,
+} from '@/lib/api/contracts/selectors/execute'
+import { MAX_PERSONAL_SOURCE_SETUP_KEYS } from '@/lib/sim-search/personal-source-setup'
+
+const setupOwnerSchema = z.object({
+ organizationId: organizationIdSchema,
+ connectorType: z.enum(['jira', 'confluence']),
+})
+const setupCredentialSchema = setupOwnerSchema.extend({
+ credentialId: z.string().min(1).max(128),
+ domain: z.string().trim().min(1, 'Enter your Atlassian site').max(253),
+})
+
+export const personalSourceSetupQuerySchema = setupOwnerSchema.extend({
+ completionId: z.string().uuid().optional(),
+})
+export type PersonalSourceSetupQuery = z.input
+
+export const personalSourceSetupAccountsSchema = z.object({
+ accounts: z
+ .array(
+ z.object({
+ id: z.string().min(1).max(128),
+ name: z.string().max(512),
+ provider: z.enum(['jira', 'confluence']),
+ type: z.literal('managed_oauth'),
+ scopes: z.array(z.string().max(200)).max(200),
+ })
+ )
+ .max(1000),
+ completedCredentialId: z.string().min(1).max(128).nullable(),
+})
+export type PersonalSourceSetupAccounts = z.output
+
+export const personalSourceSetupBodySchema = z.discriminatedUnion('action', [
+ setupOwnerSchema
+ .extend({ action: z.literal('authorize'), oauthCompletionId: z.string().uuid() })
+ .strict(),
+ setupCredentialSchema
+ .extend({
+ action: z.literal('connect'),
+ keys: z
+ .array(z.string().trim().min(1).max(255))
+ .min(1, 'Select at least one project or space')
+ .max(
+ MAX_PERSONAL_SOURCE_SETUP_KEYS,
+ 'Choose no more than 1,000 projects or spaces per source'
+ ),
+ })
+ .strict(),
+ setupCredentialSchema
+ .extend({ action: z.literal('options'), request: selectorRequestSchema })
+ .strict(),
+])
+export type PersonalSourceSetupBody = z.input
+
+export const personalSourceSetupResultSchema = z.discriminatedUnion('kind', [
+ z.object({ kind: z.literal('authorization'), url: z.string().url() }),
+ z.object({
+ kind: z.literal('connected'),
+ knowledgeBaseId: z.string().min(1).max(200),
+ connectorId: z.string().min(1).max(200),
+ }),
+ ...executeSelectorResponseSchema.options,
+])
+export type PersonalSourceSetupResult = z.output
+
+export const listPersonalSourceSetupAccountsContract = defineRouteContract({
+ method: 'GET',
+ path: '/api/knowledge/sim-search/personal-source-setup',
+ query: personalSourceSetupQuerySchema,
+ response: { mode: 'json', schema: successResponseSchema(personalSourceSetupAccountsSchema) },
+})
+
+export const personalSourceSetupContract = defineRouteContract({
+ method: 'POST',
+ path: '/api/knowledge/sim-search/personal-source-setup',
+ body: personalSourceSetupBodySchema,
+ response: { mode: 'json', schema: successResponseSchema(personalSourceSetupResultSchema) },
+})
diff --git a/apps/sim/lib/api/contracts/knowledge/search-stats.ts b/apps/sim/lib/api/contracts/knowledge/search-stats.ts
new file mode 100644
index 00000000000..63f30e2ec8f
--- /dev/null
+++ b/apps/sim/lib/api/contracts/knowledge/search-stats.ts
@@ -0,0 +1,76 @@
+import { z } from 'zod'
+import { organizationIdSchema } from '@/lib/api/contracts/primitives'
+import { defineRouteContract } from '@/lib/api/contracts/types'
+import {
+ getSearchStatsRangeError,
+ SEARCH_STATS_MAX_DAYS,
+ SEARCH_STATS_PEOPLE_LIMIT,
+ SEARCH_STATS_PERIODS,
+ SEARCH_STATS_SOURCE_LIMIT,
+ SEARCH_STATS_SURFACES,
+} from '@/lib/knowledge/search/stats'
+
+export const organizationSearchStatsQuerySchema = z
+ .object({
+ organizationId: organizationIdSchema,
+ period: z.enum(SEARCH_STATS_PERIODS).default('30d'),
+ surface: z.enum(SEARCH_STATS_SURFACES).optional(),
+ startDate: z.string().max(10).optional(),
+ endDate: z.string().max(10).optional(),
+ })
+ .superRefine((query, context) => {
+ if (query.period === 'custom') {
+ const error = getSearchStatsRangeError(query)
+ if (error) context.addIssue({ code: 'custom', path: ['startDate'], message: error })
+ } else if (query.startDate !== undefined || query.endDate !== undefined) {
+ context.addIssue({
+ code: 'custom',
+ path: ['period'],
+ message: 'Choose Custom range to use start and end dates.',
+ })
+ }
+ })
+export type OrganizationSearchStatsQuery = z.input
+
+const countSchema = z.number().int().nonnegative()
+const sourceTypeSchema = z.string().min(1).max(100)
+export const organizationSearchStatsSchema = z.object({
+ start: z.string().datetime(),
+ end: z.string().datetime(),
+ totals: z.object({
+ invocations: countSchema,
+ activePeople: countSchema,
+ results: countSchema,
+ }),
+ series: z
+ .array(z.object({ timestamp: z.string().datetime(), invocations: countSchema }))
+ .max(SEARCH_STATS_MAX_DAYS),
+ surfaces: z
+ .array(z.object({ surface: z.enum(SEARCH_STATS_SURFACES), invocations: countSchema }))
+ .max(7),
+ sources: z
+ .array(z.object({ sourceType: sourceTypeSchema, invocations: countSchema }))
+ .max(SEARCH_STATS_SOURCE_LIMIT),
+ people: z
+ .array(
+ z.object({
+ userId: z.string().nullable(),
+ name: z.string().nullable(),
+ email: z.string().nullable(),
+ invocations: countSchema,
+ sourceTypes: z.array(sourceTypeSchema).max(SEARCH_STATS_SOURCE_LIMIT),
+ })
+ )
+ .max(SEARCH_STATS_PEOPLE_LIMIT),
+})
+export type OrganizationSearchStats = z.output
+
+export const readOrganizationSearchStatsContract = defineRouteContract({
+ method: 'GET',
+ path: '/api/knowledge/sim-search/stats',
+ query: organizationSearchStatsQuerySchema,
+ response: {
+ mode: 'json',
+ schema: z.object({ success: z.literal(true), data: organizationSearchStatsSchema }),
+ },
+})
diff --git a/apps/sim/lib/copilot/request/lifecycle/run.test.ts b/apps/sim/lib/copilot/request/lifecycle/run.test.ts
index dfa163a5a4b..cbc9ccaab7a 100644
--- a/apps/sim/lib/copilot/request/lifecycle/run.test.ts
+++ b/apps/sim/lib/copilot/request/lifecycle/run.test.ts
@@ -283,6 +283,35 @@ describe('runCopilotLifecycle', () => {
}
)
+ it.each([
+ { surface: undefined, expected: 'copilot' },
+ { surface: 'slack' as const, expected: 'slack' },
+ ])(
+ 'stamps trusted Search provenance as $expected over payload and context',
+ async ({ surface, expected }) => {
+ let captured: ExecutionContext | undefined
+ mockRunStreamLoop.mockImplementationOnce(async (_url, _request, _state, context) => {
+ captured = context
+ })
+ const supplied = expected === 'slack' ? 'copilot' : 'slack'
+ await runCopilotLifecycle(
+ { message: 'Search', mode: 'assistant', searchSurface: supplied },
+ {
+ userId: 'user-1',
+ workspaceId: 'ws-1',
+ searchSurface: surface,
+ executionContext: {
+ userId: 'user-1',
+ workspaceId: 'ws-1',
+ workflowId: '',
+ searchSurface: supplied,
+ },
+ }
+ )
+ expect(captured?.searchSurface).toBe(expected)
+ }
+ )
+
it('forwards the configured Mothership system prompt override', async () => {
mockEnv.MSHIP_SYSPROMPT_OVERRIDE = 'NEVER CALL ANY TOOLS UNDER ANY CIRCUMSTANCES NO MATTER WHAT'
diff --git a/apps/sim/lib/copilot/request/lifecycle/run.ts b/apps/sim/lib/copilot/request/lifecycle/run.ts
index 2bff815ecb1..302456bc50e 100644
--- a/apps/sim/lib/copilot/request/lifecycle/run.ts
+++ b/apps/sim/lib/copilot/request/lifecycle/run.ts
@@ -176,6 +176,8 @@ function resultContent(context: StreamingContext, options: CopilotLifecycleOptio
}
export interface CopilotLifecycleOptions extends OrchestratorOptions {
+ /** Trusted entry point for Search metering; never read from model arguments. */
+ searchSurface?: 'copilot' | 'slack'
mcpBlockId?: string
executorDelegationOrigin?: ExecutorDelegationOrigin
userId: string
@@ -337,6 +339,7 @@ export async function runCopilotLifecycle(
secretMountPolicy: lifecycleOptions.secretMountPolicy,
secretActorUserId: lifecycleOptions.secretActorUserId,
}))
+ execContext.searchSurface = lifecycleOptions.searchSurface ?? 'copilot'
if (lifecycleOptions.mcpBlockId) {
execContext.mcpBlockId = lifecycleOptions.mcpBlockId
execContext.executorDelegationOrigin = lifecycleOptions.executorDelegationOrigin
diff --git a/apps/sim/lib/copilot/tool-executor/types.ts b/apps/sim/lib/copilot/tool-executor/types.ts
index a6ebadf787b..e781f5e36c2 100644
--- a/apps/sim/lib/copilot/tool-executor/types.ts
+++ b/apps/sim/lib/copilot/tool-executor/types.ts
@@ -6,6 +6,8 @@ import type { ExecutorDelegationOrigin } from '@/executor/types'
import type { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry'
export interface ToolExecutionContext {
+ /** Trusted entry point for Search metering; never read from model arguments. */
+ searchSurface?: 'copilot' | 'slack'
mcpBlockId?: string
executorDelegationOrigin?: ExecutorDelegationOrigin
userId: string
diff --git a/apps/sim/lib/copilot/tools/registry/server-tool-adapter.test.ts b/apps/sim/lib/copilot/tools/registry/server-tool-adapter.test.ts
index f65a8b2ab70..11462df50a0 100644
--- a/apps/sim/lib/copilot/tools/registry/server-tool-adapter.test.ts
+++ b/apps/sim/lib/copilot/tools/registry/server-tool-adapter.test.ts
@@ -80,4 +80,24 @@ describe('server tool adapter authority boundary', () => {
storageError
)
})
+
+ it('forwards trusted Search provenance independently of model parameters', async () => {
+ await createServerToolHandler('search_workspace')(
+ { query: 'policy', searchSurface: 'copilot', surface: 'copilot' },
+ {
+ userId: 'user-1',
+ workflowId: '',
+ organizationId: 'org-1',
+ chatId: 'chat-1',
+ toolCallId: 'call-1',
+ copilotToolExecution: true,
+ searchSurface: 'slack',
+ }
+ )
+ expect(mocks.routeExecution).toHaveBeenCalledWith(
+ 'search_workspace',
+ expect.objectContaining({ query: 'policy' }),
+ expect.objectContaining({ searchSurface: 'slack', organizationId: 'org-1' })
+ )
+ })
})
diff --git a/apps/sim/lib/copilot/tools/registry/server-tool-adapter.ts b/apps/sim/lib/copilot/tools/registry/server-tool-adapter.ts
index 5742c431148..f7c37bbf2bd 100644
--- a/apps/sim/lib/copilot/tools/registry/server-tool-adapter.ts
+++ b/apps/sim/lib/copilot/tools/registry/server-tool-adapter.ts
@@ -24,6 +24,7 @@ export function createServerToolHandler(toolId: string): ToolHandler {
toolCallId: context.toolCallId,
copilotToolExecution: context.copilotToolExecution,
requestMode: context.requestMode,
+ searchSurface: context.searchSurface,
assistantSearch: context.assistantSearch,
billingAttribution: context.billingAttribution,
userPermission: context.userPermission ?? undefined,
diff --git a/apps/sim/lib/copilot/tools/server/base-tool.ts b/apps/sim/lib/copilot/tools/server/base-tool.ts
index eacf3d152b4..96b6df6f3bd 100644
--- a/apps/sim/lib/copilot/tools/server/base-tool.ts
+++ b/apps/sim/lib/copilot/tools/server/base-tool.ts
@@ -4,6 +4,8 @@ import type { WorkspaceSearchFilters } from '@/lib/knowledge/search/filters'
import type { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry'
export interface ServerToolContext {
+ /** Trusted entry point for Search metering; never read from model arguments. */
+ searchSurface?: 'copilot' | 'slack'
requestMode?: string
assistantSearch?: WorkspaceSearchFilters
userId: string
diff --git a/apps/sim/lib/copilot/tools/server/knowledge/knowledge-base.test.ts b/apps/sim/lib/copilot/tools/server/knowledge/knowledge-base.test.ts
index 17b3a987c3b..298d5d7f270 100644
--- a/apps/sim/lib/copilot/tools/server/knowledge/knowledge-base.test.ts
+++ b/apps/sim/lib/copilot/tools/server/knowledge/knowledge-base.test.ts
@@ -420,6 +420,22 @@ describe('manage_knowledge_base trusted application delegation', () => {
expect(mockReadKnowledgeBase).not.toHaveBeenCalled()
})
+ it('attributes knowledge queries to the trusted Slack context', async () => {
+ const result = await knowledgeBaseServerTool.execute(
+ { operation: 'query', args: { knowledgeBaseId: KNOWLEDGE_BASE.id, query: 'query' } },
+ {
+ ...CONTEXT,
+ resolvedSecretTraceRegistry: new ResolvedSecretTraceRegistry(),
+ searchSurface: 'slack',
+ }
+ )
+
+ expect(result.success).toBe(true)
+ expect(mockSearchKnowledge).toHaveBeenCalledWith(
+ expect.objectContaining({ input: expect.objectContaining({ surface: 'slack' }) })
+ )
+ })
+
it('asks for citations where per-member access is on', async () => {
const result = await knowledgeBaseServerTool.execute(
{ operation: 'query', args: { knowledgeBaseId: KNOWLEDGE_BASE.id, query: 'query' } },
diff --git a/apps/sim/lib/copilot/tools/server/knowledge/knowledge-base.ts b/apps/sim/lib/copilot/tools/server/knowledge/knowledge-base.ts
index 4781eb1ca8a..a610f02b6b2 100644
--- a/apps/sim/lib/copilot/tools/server/knowledge/knowledge-base.ts
+++ b/apps/sim/lib/copilot/tools/server/knowledge/knowledge-base.ts
@@ -449,7 +449,7 @@ export const knowledgeBaseServerTool: BaseServerTool {
})
})
+ it.each([
+ { searchSurface: undefined, expected: 'copilot' },
+ { searchSurface: 'slack' as const, expected: 'slack' },
+ ])(
+ 'attributes organization searches to trusted $expected provenance',
+ async ({ searchSurface, expected }) => {
+ const result = await searchWorkspaceServerTool.execute(
+ { query: 'policy', surface: 'api', searchSurface: 'api' },
+ {
+ ...context,
+ workspaceId: undefined,
+ organizationId: 'org-1',
+ chatId: 'chat-1',
+ requestMode: 'assistant',
+ searchSurface,
+ }
+ )
+ expect(result).toMatchObject({ success: true })
+ expect(mocks.search).toHaveBeenCalledWith({
+ principal: expect.objectContaining({ organizationId: 'org-1', subjectUserId: 'reader' }),
+ input: expect.objectContaining({ surface: expected, organizationId: 'org-1' }),
+ })
+ }
+ )
+
it('rejects a removed member or another private chat before reading documents', async () => {
mocks.authorizeChat.mockRejectedValueOnce(new Error('Conversation not found'))
const result = await readDocumentServerTool.execute(
diff --git a/apps/sim/lib/copilot/tools/server/knowledge/workspace-search.ts b/apps/sim/lib/copilot/tools/server/knowledge/workspace-search.ts
index e5bc7664f7d..4fad2478865 100644
--- a/apps/sim/lib/copilot/tools/server/knowledge/workspace-search.ts
+++ b/apps/sim/lib/copilot/tools/server/knowledge/workspace-search.ts
@@ -53,7 +53,7 @@ export const searchWorkspaceServerTool: BaseServerTool = {
query: projected.value,
topK,
filters: intersectWorkspaceSearchFilters(requestedFilters, context?.assistantSearch),
- surface: 'copilot',
+ surface: context?.searchSurface ?? 'copilot',
resultSecretRegistry: registry,
signal: context?.abortSignal,
} as const
diff --git a/apps/sim/lib/credential-groups/service.ts b/apps/sim/lib/credential-groups/service.ts
index 79f2e428378..c33f52a54db 100644
--- a/apps/sim/lib/credential-groups/service.ts
+++ b/apps/sim/lib/credential-groups/service.ts
@@ -307,7 +307,7 @@ export async function ensureWorkspaceAccountsGroup(
throw new OrchestrationError(
'validation',
scope.kind === 'organization'
- ? `Refresh ${preparedOption.label} in Sources > More > Refresh connection settings, or Connected accounts > Providers > Update configurations when Search is disabled`
+ ? `Refresh ${preparedOption.label} in Sources > More > Update sign-in settings, or Connected accounts > Providers > Update configurations when Search is disabled`
: `Update ${preparedOption.label} in Connected accounts before connecting this source`
)
}
diff --git a/apps/sim/lib/knowledge/application/operations.test.ts b/apps/sim/lib/knowledge/application/operations.test.ts
index 1dc78ba611f..a2d8f28daaf 100644
--- a/apps/sim/lib/knowledge/application/operations.test.ts
+++ b/apps/sim/lib/knowledge/application/operations.test.ts
@@ -78,10 +78,13 @@ describe('knowledge operation registry', () => {
'knowledge.search.sources.progress',
'knowledge.search.integrations.list',
'knowledge.search.integrations.overview',
+ 'knowledge.search.stats.read',
'knowledge.search.integrations.approve',
'knowledge.connectors.members.list',
'knowledge.connectors.members.enroll',
'knowledge.simSearch.connect',
+ 'knowledge.search.personalSetup.accounts.list',
+ 'knowledge.search.personalSetup',
'knowledge.search.sources.connectApproved',
'knowledge.search.index.read',
'knowledge.search.sources.prepare',
@@ -143,11 +146,25 @@ describe('knowledge operation registry', () => {
knowledgeOperations.readSearchIndex,
knowledgeOperations.enrollConnectorMember,
knowledgeOperations.simSearchConnect,
+ knowledgeOperations.listPersonalSourceSetupAccounts,
+ knowledgeOperations.personalSourceSetup,
]) {
expect(operation.organizationOperation.minimumRole).toBe('member')
}
})
+ it('limits personal source setup to the signed-in member without delegating credential discovery', () => {
+ for (const operation of [
+ knowledgeOperations.listPersonalSourceSetupAccounts,
+ knowledgeOperations.personalSourceSetup,
+ ]) {
+ expect(operation.principalKinds).toEqual(['session'])
+ expect(operation.organizationOperation.principalKinds).not.toContain('organization_delegated')
+ expect(operation.workspaceApiKey).toBe('deny')
+ expect(operation.capability).toBe('knowledge.use')
+ }
+ })
+
it('permits organization delegation only for Copilot reads', () => {
for (const operation of Object.values(knowledgeOperations)) {
if (!operation.organizationOperation.principalKinds.includes('organization_delegated'))
diff --git a/apps/sim/lib/knowledge/application/operations.ts b/apps/sim/lib/knowledge/application/operations.ts
index e9a7e1cb9a7..87d98e6e53c 100644
--- a/apps/sim/lib/knowledge/application/operations.ts
+++ b/apps/sim/lib/knowledge/application/operations.ts
@@ -758,6 +758,15 @@ export const knowledgeOperations = {
principalKinds: ['session'],
})
),
+ readOrganizationSearchStats: defineKnowledgeOperation(
+ defineWorkspaceOperation({
+ id: 'knowledge.search.stats.read',
+ minimumRole: 'admin',
+ workspaceApiKey: 'deny',
+ capability: 'knowledge.use',
+ principalKinds: ['session'],
+ })
+ ),
approveSearchIntegration: defineKnowledgeOperation(
defineWorkspaceOperation({
id: 'knowledge.search.integrations.approve',
@@ -805,6 +814,24 @@ export const knowledgeOperations = {
principalKinds: ['session'],
})
),
+ listPersonalSourceSetupAccounts: defineKnowledgeOperation(
+ defineWorkspaceOperation({
+ id: 'knowledge.search.personalSetup.accounts.list',
+ minimumRole: 'read',
+ workspaceApiKey: 'deny',
+ capability: 'knowledge.use',
+ principalKinds: ['session'],
+ })
+ ),
+ personalSourceSetup: defineKnowledgeOperation(
+ defineWorkspaceOperation({
+ id: 'knowledge.search.personalSetup',
+ minimumRole: 'read',
+ workspaceApiKey: 'deny',
+ capability: 'knowledge.use',
+ principalKinds: ['session'],
+ })
+ ),
createApprovedSearchSource: defineKnowledgeOperation(
defineWorkspaceOperation({
id: 'knowledge.search.sources.connectApproved',
diff --git a/apps/sim/lib/knowledge/application/organization-search-stats.test.ts b/apps/sim/lib/knowledge/application/organization-search-stats.test.ts
new file mode 100644
index 00000000000..7b7a2b97b30
--- /dev/null
+++ b/apps/sim/lib/knowledge/application/organization-search-stats.test.ts
@@ -0,0 +1,82 @@
+/** @vitest-environment node */
+import { member } from '@sim/db/schema'
+import { queueTableRows, resetDbChainMock } from '@sim/testing'
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+
+const mocks = vi.hoisted(() => ({
+ context: vi.fn(),
+ policy: vi.fn(),
+ available: vi.fn(),
+ load: vi.fn(),
+}))
+vi.mock('@/lib/knowledge/application/contexts', () => ({
+ resolveKnowledgeOwnerContext: mocks.context,
+}))
+vi.mock('@/lib/permission-groups/resolve.server', () => ({
+ getUserPermissionConfigForOrganization: mocks.policy,
+}))
+vi.mock('@sim/platform-authz/workspace', () => ({
+ isOrgAdminRole: (role: string) => role === 'admin' || role === 'owner',
+}))
+vi.mock('@/lib/knowledge/access/availability', () => ({
+ requireOrganizationSearchAvailable: mocks.available,
+}))
+vi.mock('@/lib/knowledge/search/activity-stats', () => ({
+ loadOrganizationSearchStats: mocks.load,
+}))
+
+import { readOrganizationSearchStats } from '@/lib/knowledge/application/organization-search-stats'
+
+const principal = { kind: 'session', userId: 'admin', sessionId: 'session' } as const
+const input = { organizationId: 'organization', period: '7d', surface: 'mcp' } as const
+
+beforeEach(() => {
+ vi.clearAllMocks()
+ resetDbChainMock()
+ mocks.context.mockResolvedValue({ organizationId: 'organization' })
+ mocks.policy.mockResolvedValue(null)
+ mocks.available.mockResolvedValue(undefined)
+ mocks.load.mockResolvedValue({ totals: { invocations: 3 } })
+})
+
+describe('organization Search stats authorization', () => {
+ it.each(['admin', 'owner'])(
+ 'allows a current %s and forwards the selected scope',
+ async (role) => {
+ queueTableRows(member, [{ role }])
+ expect(await readOrganizationSearchStats.execute({ principal, input })).toEqual({
+ totals: { invocations: 3 },
+ })
+ expect(mocks.available).toHaveBeenCalledWith('organization')
+ expect(mocks.load).toHaveBeenCalledWith(input)
+ }
+ )
+ it.each([
+ { rows: [{ role: 'member' }], code: 'forbidden' },
+ { rows: [], code: 'not_found' },
+ ])('rejects unauthorized access with $code before aggregation', async ({ rows, code }) => {
+ queueTableRows(member, rows)
+ await expect(readOrganizationSearchStats.execute({ principal, input })).rejects.toMatchObject({
+ code,
+ })
+ expect(mocks.load).not.toHaveBeenCalled()
+ })
+ it('rejects API keys before protected loading', async () => {
+ await expect(
+ readOrganizationSearchStats.execute({
+ principal: { kind: 'personal_api_key', userId: 'admin', keyId: 'key' },
+ input,
+ })
+ ).rejects.toThrow()
+ expect(mocks.context).not.toHaveBeenCalled()
+ expect(mocks.load).not.toHaveBeenCalled()
+ })
+ it('fails closed when Search is disabled', async () => {
+ queueTableRows(member, [{ role: 'admin' }])
+ mocks.available.mockRejectedValueOnce(new Error('Search is disabled'))
+ await expect(readOrganizationSearchStats.execute({ principal, input })).rejects.toThrow(
+ 'Search is disabled'
+ )
+ expect(mocks.load).not.toHaveBeenCalled()
+ })
+})
diff --git a/apps/sim/lib/knowledge/application/organization-search-stats.ts b/apps/sim/lib/knowledge/application/organization-search-stats.ts
new file mode 100644
index 00000000000..98aabe2973c
--- /dev/null
+++ b/apps/sim/lib/knowledge/application/organization-search-stats.ts
@@ -0,0 +1,21 @@
+import { OrchestrationError } from '@/lib/core/orchestration/types'
+import { requireOrganizationSearchAvailable } from '@/lib/knowledge/access/availability'
+import { defineAuthorizedKnowledgeUseCase } from '@/lib/knowledge/application/authorized-knowledge-use-case'
+import { resolveKnowledgeOwnerContext } from '@/lib/knowledge/application/contexts'
+import { knowledgeOperations } from '@/lib/knowledge/application/operations'
+import {
+ loadOrganizationSearchStats,
+ type SearchStatsInput,
+} from '@/lib/knowledge/search/activity-stats'
+
+export const readOrganizationSearchStats = defineAuthorizedKnowledgeUseCase({
+ operation: knowledgeOperations.readOrganizationSearchStats,
+ resolveContext: ({ input }: { input: SearchStatsInput }) =>
+ resolveKnowledgeOwnerContext({ organizationId: input.organizationId }),
+ async execute({ context, input }) {
+ if (!context.organizationId)
+ throw new OrchestrationError('validation', 'Organization is required')
+ await requireOrganizationSearchAvailable(context.organizationId)
+ return loadOrganizationSearchStats({ ...input, organizationId: context.organizationId })
+ },
+})
diff --git a/apps/sim/lib/knowledge/application/personal-search-account.test.ts b/apps/sim/lib/knowledge/application/personal-search-account.test.ts
new file mode 100644
index 00000000000..a927fb31bfc
--- /dev/null
+++ b/apps/sim/lib/knowledge/application/personal-search-account.test.ts
@@ -0,0 +1,133 @@
+/** @vitest-environment node */
+import { user } from '@sim/db/schema'
+import { dbChainMockFns, queueTableRows, resetDbChainMock } from '@sim/testing'
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+
+const mocks = vi.hoisted(() => ({
+ membership: vi.fn(),
+ approval: vi.fn(),
+ available: vi.fn(),
+ deployed: vi.fn(),
+ accounts: vi.fn(),
+}))
+vi.mock('@/lib/core/application/organization-authorization', () => ({
+ requireOrganizationMembership: mocks.membership,
+}))
+vi.mock('@/lib/knowledge/search/integration-policy', () => ({
+ requireOrganizationSearchApproval: mocks.approval,
+}))
+vi.mock('@/lib/knowledge/access/availability', () => ({
+ requireKnowledgeMemberAccessAvailable: mocks.available,
+}))
+vi.mock('@/lib/integrations/availability.server', () => ({
+ isOAuthServiceDeploymentAvailable: mocks.deployed,
+}))
+vi.mock('@/lib/credentials/organization-managed', () => ({
+ getOwnOrganizationManagedOAuthCredentials: mocks.accounts,
+}))
+
+import {
+ authorizePersonalSearchSetup,
+ authorizePersonalSearchSetupCredential,
+} from '@/lib/knowledge/application/personal-search-account'
+
+const principal = { kind: 'session', userId: 'member-1', sessionId: 'session-1' } as const
+const input = {
+ organizationId: 'organization-1',
+ connectorType: 'jira',
+ credentialId: 'own-account',
+} as const
+
+describe('personal Search setup authorization', () => {
+ beforeEach(() => {
+ vi.resetAllMocks()
+ resetDbChainMock()
+ queueTableRows(user, [{ emailVerified: true }])
+ mocks.deployed.mockReturnValue(true)
+ mocks.accounts.mockResolvedValue([
+ {
+ id: 'own-account',
+ providerId: 'jira',
+ displayName: 'Work account',
+ scopes: ['read:jira-work'],
+ },
+ ])
+ })
+
+ it('permits a verified member and scopes the account lookup to that person and provider', async () => {
+ await expect(authorizePersonalSearchSetupCredential(principal, input)).resolves.toMatchObject({
+ id: 'own-account',
+ })
+ expect(mocks.membership).toHaveBeenCalledWith(
+ principal,
+ input.organizationId,
+ 'member',
+ 'knowledge.use'
+ )
+ expect(mocks.approval).toHaveBeenCalledWith(input.organizationId, 'jira')
+ expect(mocks.accounts).toHaveBeenCalledWith({
+ organizationId: input.organizationId,
+ userId: principal.userId,
+ providerId: 'jira',
+ credentialId: input.credentialId,
+ })
+ })
+
+ it('rejects non-session callers before membership or protected account reads', async () => {
+ await expect(
+ authorizePersonalSearchSetup(
+ { kind: 'personal_api_key', userId: 'member-1', keyId: 'key-1' },
+ input
+ )
+ ).rejects.toThrow('Sign in')
+ expect(mocks.membership).not.toHaveBeenCalled()
+ expect(dbChainMockFns.select).not.toHaveBeenCalled()
+ })
+
+ it('rejects non-members before looking up credentials', async () => {
+ mocks.membership.mockRejectedValue(new Error('Membership ended'))
+ await expect(authorizePersonalSearchSetupCredential(principal, input)).rejects.toThrow(
+ 'Membership ended'
+ )
+ expect(mocks.accounts).not.toHaveBeenCalled()
+ })
+
+ it('requires a verified email before approving or provisioning setup', async () => {
+ resetDbChainMock()
+ queueTableRows(user, [{ emailVerified: false }])
+ await expect(authorizePersonalSearchSetup(principal, input)).rejects.toThrow(
+ 'Verify your email'
+ )
+ expect(mocks.approval).not.toHaveBeenCalled()
+ })
+
+ it.each(['approval', 'available'] as const)(
+ 'propagates the %s refusal before account discovery',
+ async (gate) => {
+ mocks[gate].mockRejectedValue(new Error('Source disabled'))
+ await expect(authorizePersonalSearchSetupCredential(principal, input)).rejects.toThrow(
+ 'Source disabled'
+ )
+ expect(mocks.accounts).not.toHaveBeenCalled()
+ }
+ )
+
+ it('rejects an unavailable OAuth deployment', async () => {
+ mocks.deployed.mockReturnValue(false)
+ await expect(authorizePersonalSearchSetupCredential(principal, input)).rejects.toThrow(
+ 'unavailable'
+ )
+ expect(mocks.accounts).not.toHaveBeenCalled()
+ })
+
+ it.each([
+ { accounts: [] },
+ { accounts: [{ id: 'another-account', providerId: 'jira' }] },
+ { accounts: [{ id: 'own-account', providerId: 'confluence' }] },
+ ])('rejects revoked, foreign or mismatched grants %#', async ({ accounts }) => {
+ mocks.accounts.mockResolvedValue(accounts)
+ await expect(authorizePersonalSearchSetupCredential(principal, input)).rejects.toThrow(
+ 'Connect your account again'
+ )
+ })
+})
diff --git a/apps/sim/lib/knowledge/application/personal-search-account.ts b/apps/sim/lib/knowledge/application/personal-search-account.ts
new file mode 100644
index 00000000000..04a25443acb
--- /dev/null
+++ b/apps/sim/lib/knowledge/application/personal-search-account.ts
@@ -0,0 +1,68 @@
+import type { Principal } from '@sim/auth/principal'
+import { db } from '@sim/db'
+import { user } from '@sim/db/schema'
+import { eq } from 'drizzle-orm'
+import { requireOrganizationMembership } from '@/lib/core/application/organization-authorization'
+import { OrchestrationError } from '@/lib/core/orchestration/types'
+import { getOwnOrganizationManagedOAuthCredentials } from '@/lib/credentials/organization-managed'
+import { isOAuthServiceDeploymentAvailable } from '@/lib/integrations/availability.server'
+import { requireKnowledgeMemberAccessAvailable } from '@/lib/knowledge/access/availability'
+import { requireOrganizationSearchApproval } from '@/lib/knowledge/search/integration-policy'
+
+export type PersonalSearchSetupConnector = 'jira' | 'confluence'
+
+/** Rechecks the caller's organization and approved integration before personal account discovery. */
+export async function authorizePersonalSearchSetup(
+ principal: Principal,
+ input: { organizationId: string; connectorType: PersonalSearchSetupConnector }
+) {
+ if (
+ principal.kind !== 'session' ||
+ (input.connectorType !== 'jira' && input.connectorType !== 'confluence')
+ ) {
+ throw new OrchestrationError('forbidden', 'Sign in to connect this Search source')
+ }
+ await requireOrganizationMembership(principal, input.organizationId, 'member', 'knowledge.use')
+ const [viewer] = await db
+ .select({ emailVerified: user.emailVerified })
+ .from(user)
+ .where(eq(user.id, principal.userId))
+ .limit(1)
+ if (!viewer?.emailVerified) {
+ throw new OrchestrationError(
+ 'validation',
+ 'Verify your email address before connecting an account'
+ )
+ }
+ await requireOrganizationSearchApproval(input.organizationId, input.connectorType)
+ await requireKnowledgeMemberAccessAvailable({ organizationId: input.organizationId })
+ if (!isOAuthServiceDeploymentAvailable(input.connectorType)) {
+ throw new OrchestrationError('validation', 'This account connection is unavailable')
+ }
+ return principal.userId
+}
+
+/** Personal setup can browse only a currently live grant owned by the signed-in member. */
+export async function authorizePersonalSearchSetupCredential(
+ principal: Principal,
+ input: {
+ organizationId: string
+ connectorType: PersonalSearchSetupConnector
+ credentialId: string
+ }
+) {
+ const userId = await authorizePersonalSearchSetup(principal, input)
+ const accounts = await getOwnOrganizationManagedOAuthCredentials({
+ organizationId: input.organizationId,
+ userId,
+ providerId: input.connectorType,
+ credentialId: input.credentialId,
+ })
+ const account = accounts.find(
+ (entry) => entry.id === input.credentialId && entry.providerId === input.connectorType
+ )
+ if (!account) {
+ throw new OrchestrationError('not_found', 'Connect your account again before choosing sources')
+ }
+ return account
+}
diff --git a/apps/sim/lib/knowledge/application/personal-source-setup.test.ts b/apps/sim/lib/knowledge/application/personal-source-setup.test.ts
new file mode 100644
index 00000000000..9ae1a409a4d
--- /dev/null
+++ b/apps/sim/lib/knowledge/application/personal-source-setup.test.ts
@@ -0,0 +1,480 @@
+/** @vitest-environment node */
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+
+const mocks = vi.hoisted(() => ({
+ authorizeOperation: vi.fn(),
+ authorize: vi.fn(),
+ ownAccount: vi.fn(),
+ listAccounts: vi.fn(),
+ completion: vi.fn(),
+ provision: vi.fn(),
+ enrollment: vi.fn(),
+ oauthContext: vi.fn(),
+ oauth: vi.fn(),
+ selector: vi.fn(),
+ configure: vi.fn(),
+ binding: vi.fn(),
+ group: vi.fn(),
+ dispatch: vi.fn(),
+ billing: vi.fn(),
+}))
+vi.mock('@/lib/core/application/organization-authorization', () => ({
+ authorizeOrganizationOperation: mocks.authorizeOperation,
+}))
+vi.mock('@/lib/knowledge/application/contexts', () => ({
+ resolveKnowledgeOrganizationContext: async ({ organizationId }: { organizationId: string }) => ({
+ organizationId,
+ workspaceId: undefined,
+ }),
+}))
+vi.mock('@/lib/knowledge/application/personal-search-account', () => ({
+ authorizePersonalSearchSetup: mocks.authorize,
+ authorizePersonalSearchSetupCredential: mocks.ownAccount,
+}))
+vi.mock('@/lib/credentials/organization-managed', () => ({
+ getOwnOrganizationManagedOAuthCredentials: mocks.listAccounts,
+}))
+vi.mock('@/lib/credential-groups/search-connection-completion', () => ({
+ readSearchConnectionCompletion: mocks.completion,
+}))
+vi.mock('@/lib/knowledge/connectors/member-provisioning', () => ({
+ provisionKnowledgeConnectorMembersBinding: mocks.provision,
+}))
+vi.mock('@/lib/credential-groups/self-enrollment', () => ({
+ createViewerCredentialGroupEnrollment: mocks.enrollment,
+}))
+vi.mock('@/lib/credential-groups/enrollments', () => ({
+ getCredentialGroupOAuthContextForEnrollment: mocks.oauthContext,
+}))
+vi.mock('@/lib/credential-groups/oauth', () => ({ startCredentialGroupOAuth: mocks.oauth }))
+vi.mock('@/lib/selectors/application/execute-selector', () => ({
+ executeSelector: { execute: mocks.selector },
+}))
+vi.mock('@/lib/knowledge/application/sim-search', () => ({
+ configureSimSearchConnector: { execute: mocks.configure },
+}))
+vi.mock('@/lib/credential-groups/credentials', () => ({
+ loadManagedCredentialGroupBinding: mocks.binding,
+ loadScopedAccountsCredentialListContext: mocks.group,
+}))
+vi.mock('@/lib/knowledge/connectors/member-queue', () => ({ dispatchMemberSync: mocks.dispatch }))
+vi.mock('@/lib/knowledge/application/billing', () => ({
+ resolveKnowledgeBillingAttribution: mocks.billing,
+}))
+vi.mock('@/connectors/registry', () => ({
+ CONNECTOR_META_REGISTRY: { jira: { name: 'Jira' }, confluence: { name: 'Confluence' } },
+}))
+
+import {
+ listPersonalSourceSetupAccounts,
+ personalSourceSetup,
+} from '@/lib/knowledge/application/personal-source-setup'
+import { MAX_SELECTOR_PAGES } from '@/lib/selectors/limits'
+import type { SelectorRequest } from '@/lib/selectors/types'
+
+interface ValidationSelectorCall {
+ input: { request: SelectorRequest; signal: AbortSignal }
+}
+
+const principal = { kind: 'session', userId: 'member-1', sessionId: 'session-1' } as const
+const owner = { organizationId: 'organization-1', connectorType: 'jira' } as const
+const credential = { credentialId: 'own-account', domain: 'example.atlassian.net' }
+const connect = { ...owner, ...credential, action: 'connect', keys: ['PROJECT'] } as const
+const account = {
+ id: 'own-account',
+ displayName: 'Work account',
+ providerId: 'jira',
+ scopes: ['read:jira-work'],
+}
+const runConnect = (changes = {}) =>
+ personalSourceSetup.execute({ principal, input: { ...connect, keys: ['PROJECT'], ...changes } })
+
+describe('personal source setup', () => {
+ afterEach(() => {
+ vi.restoreAllMocks()
+ })
+
+ beforeEach(() => {
+ vi.resetAllMocks()
+ mocks.authorize.mockResolvedValue(principal.userId)
+ mocks.ownAccount.mockResolvedValue(account)
+ mocks.listAccounts.mockResolvedValue([account])
+ mocks.completion.mockResolvedValue(account.id)
+ mocks.provision.mockResolvedValue({
+ credentialGroupId: 'group-1',
+ credentialGroupOptionId: 'option-1',
+ })
+ mocks.enrollment.mockResolvedValue({
+ enrollment: { id: 'enrollment-1', email: 'member@example.com' },
+ invitationLink: 'https://example.com/credential-groups/enroll/invitation-token',
+ })
+ mocks.oauthContext.mockResolvedValue({ option: { id: 'option-1' } })
+ mocks.oauth.mockResolvedValue('https://auth.atlassian.com/authorize')
+ mocks.selector.mockResolvedValue({ kind: 'list', items: [{ id: 'PROJECT', label: 'Project' }] })
+ mocks.configure.mockResolvedValue({ knowledgeBaseId: 'kb-1', connectorId: 'source-1' })
+ mocks.binding.mockResolvedValue({
+ organizationId: owner.organizationId,
+ credentialGroupId: 'group-1',
+ credentialGroupOptionId: 'option-1',
+ })
+ mocks.group.mockResolvedValue({ credentialGroupId: 'group-1' })
+ mocks.billing.mockResolvedValue({ actorUserId: principal.userId })
+ mocks.dispatch.mockResolvedValue({ queued: true })
+ })
+
+ it('lists owned accounts before a source exists and scopes completion receipts to the viewer', async () => {
+ const result = await listPersonalSourceSetupAccounts.execute({
+ principal,
+ input: { ...owner, completionId: 'completion-1' },
+ })
+ expect(result).toEqual({
+ accounts: [
+ {
+ id: 'own-account',
+ name: 'Work account',
+ provider: 'jira',
+ type: 'managed_oauth',
+ scopes: ['read:jira-work'],
+ },
+ ],
+ completedCredentialId: 'own-account',
+ })
+ expect(mocks.completion).toHaveBeenCalledWith({
+ organizationId: owner.organizationId,
+ userId: principal.userId,
+ completionId: 'completion-1',
+ })
+ expect(mocks.configure).not.toHaveBeenCalled()
+ })
+
+ it('does not return a receipt for a foreign or no longer active credential', async () => {
+ mocks.completion.mockResolvedValue('another-account')
+ expect(
+ (
+ await listPersonalSourceSetupAccounts.execute({
+ principal,
+ input: { ...owner, completionId: 'completion-1' },
+ })
+ ).completedCredentialId
+ ).toBeNull()
+ })
+
+ it('starts verified personal enrollment without creating an index or requiring project keys', async () => {
+ const result = await personalSourceSetup.execute({
+ principal,
+ input: { ...owner, action: 'authorize', oauthCompletionId: 'completion-1' },
+ })
+ expect(result).toEqual({ kind: 'authorization', url: 'https://auth.atlassian.com/authorize' })
+ expect(mocks.enrollment).toHaveBeenCalledWith({
+ organizationId: owner.organizationId,
+ userId: principal.userId,
+ credentialGroupId: 'group-1',
+ })
+ expect(mocks.oauth).toHaveBeenCalledWith({ option: { id: 'option-1' } }, 'invitation-token', {
+ completionRedirect: true,
+ returnTo: 'search',
+ completionId: 'completion-1',
+ connectionIntent: { kind: 'create' },
+ })
+ expect(mocks.configure).not.toHaveBeenCalled()
+ expect(mocks.selector).not.toHaveBeenCalled()
+ })
+
+ it('does not start OAuth if enrollment was revoked', async () => {
+ mocks.enrollment.mockRejectedValue(new Error('An admin removed your access'))
+ await expect(
+ personalSourceSetup.execute({
+ principal,
+ input: { ...owner, action: 'authorize', oauthCompletionId: 'completion-1' },
+ })
+ ).rejects.toThrow('admin removed')
+ expect(mocks.oauth).not.toHaveBeenCalled()
+ })
+
+ it.each(['jira', 'confluence'] as const)(
+ 'dispatches %s discovery through its fixed shared selector',
+ async (connectorType) => {
+ await personalSourceSetup.execute({
+ principal,
+ input: {
+ ...owner,
+ ...credential,
+ connectorType,
+ action: 'options',
+ request: { kind: 'list', cursor: 'page-2' },
+ },
+ })
+ expect(mocks.selector).toHaveBeenCalledWith({
+ principal,
+ request: undefined,
+ input: {
+ selectorKey: connectorType === 'jira' ? 'jira.projectKeys' : 'confluence.spaces',
+ scope: { kind: 'organization', organizationId: owner.organizationId },
+ context: { oauthCredential: credential.credentialId, domain: credential.domain },
+ personalSearchSetup: connectorType,
+ request: { kind: 'list', cursor: 'page-2' },
+ },
+ })
+ }
+ )
+
+ it('reuses a prepared account, validates all selected keys, and creates the source without another OAuth attempt', async () => {
+ mocks.selector
+ .mockResolvedValueOnce({
+ kind: 'list',
+ items: [{ id: 'PROJECT', label: 'Project' }],
+ nextCursor: '50',
+ })
+ .mockResolvedValueOnce({ kind: 'list', items: [{ id: 'SECOND', label: 'Second' }] })
+ await expect(runConnect({ keys: ['PROJECT', 'SECOND', 'PROJECT'] })).resolves.toEqual({
+ kind: 'connected',
+ knowledgeBaseId: 'kb-1',
+ connectorId: 'source-1',
+ })
+ expect(mocks.selector).toHaveBeenCalledTimes(2)
+ expect(mocks.configure).toHaveBeenCalledWith({
+ principal,
+ request: undefined,
+ input: {
+ organizationId: owner.organizationId,
+ connectorType: 'jira',
+ memberCredentialBinding: {
+ credentialGroupId: 'group-1',
+ credentialGroupOptionId: 'option-1',
+ },
+ sourceConfig: { domain: credential.domain, projectKey: 'PROJECT,SECOND' },
+ },
+ })
+ expect(mocks.dispatch).toHaveBeenCalledWith('source-1', {
+ billingAttribution: { actorUserId: principal.userId },
+ })
+ expect(mocks.oauth).not.toHaveBeenCalled()
+ })
+
+ it.each([
+ { kind: 'list', items: [], truncated: true, nextCursor: '50' },
+ { kind: 'list', items: [] },
+ ])('rejects unavailable manually entered keys before source creation %#', async (page) => {
+ mocks.selector.mockResolvedValueOnce(page).mockResolvedValue({ kind: 'detail', item: null })
+ await expect(runConnect({ keys: ['MISSING'] })).rejects.toThrow('could not be found')
+ expect(mocks.configure).not.toHaveBeenCalled()
+ })
+
+ it.each(['jira', 'confluence'] as const)(
+ 'verifies a manually entered %s key outside the available listing through the same authorized selector',
+ async (connectorType) => {
+ mocks.selector
+ .mockResolvedValueOnce({ kind: 'list', items: [] })
+ .mockResolvedValueOnce({ kind: 'detail', item: { id: 'PROJECT', label: 'Project' } })
+ await expect(runConnect({ connectorType })).resolves.toMatchObject({ kind: 'connected' })
+ expect(mocks.selector).toHaveBeenLastCalledWith({
+ principal,
+ request: undefined,
+ input: {
+ selectorKey: connectorType === 'jira' ? 'jira.projectKeys' : 'confluence.spaces',
+ scope: { kind: 'organization', organizationId: owner.organizationId },
+ context: { oauthCredential: credential.credentialId, domain: credential.domain },
+ personalSearchSetup: connectorType,
+ signal: expect.any(AbortSignal),
+ request: { kind: 'detail', id: 'PROJECT' },
+ },
+ })
+ expect(mocks.ownAccount).toHaveBeenCalledTimes(2)
+ expect(mocks.oauth).not.toHaveBeenCalled()
+ }
+ )
+
+ it.each([
+ { name: 'truncated results', pages: 1, truncated: true, nextCursor: '50' },
+ { name: 'a repeated cursor', pages: 2, nextCursor: '50' },
+ { name: 'the page limit', pages: MAX_SELECTOR_PAGES },
+ ])('resolves remaining keys directly after $name', async ({ pages, truncated, nextCursor }) => {
+ let listed = 0
+ mocks.selector.mockImplementation(({ input }: ValidationSelectorCall) => {
+ if (input.request.kind === 'detail') {
+ return { kind: 'detail', item: { id: input.request.id, label: 'Project' } }
+ }
+ listed++
+ return { kind: 'list', items: [], nextCursor: nextCursor ?? String(listed), truncated }
+ })
+ await expect(runConnect()).resolves.toMatchObject({ kind: 'connected' })
+ expect(listed).toBe(pages)
+ expect(mocks.selector).toHaveBeenCalledTimes(pages + 1)
+ expect(mocks.oauth).not.toHaveBeenCalled()
+ })
+
+ it('gives direct validation a fresh deadline when listing times out', async () => {
+ const listing = new AbortController()
+ const details = new AbortController()
+ vi.spyOn(AbortSignal, 'timeout')
+ .mockReturnValueOnce(listing.signal)
+ .mockReturnValueOnce(details.signal)
+ mocks.selector
+ .mockImplementationOnce(({ input }: ValidationSelectorCall) => {
+ listing.abort(new DOMException('Listing timed out', 'TimeoutError'))
+ input.signal.throwIfAborted()
+ })
+ .mockImplementationOnce(({ input }: ValidationSelectorCall) => {
+ expect(input.signal.aborted).toBe(false)
+ expect(input.request).toEqual({ kind: 'detail', id: 'PROJECT' })
+ return { kind: 'detail', item: { id: 'PROJECT', label: 'Project' } }
+ })
+ await expect(runConnect()).resolves.toMatchObject({ kind: 'connected' })
+ expect(AbortSignal.timeout).toHaveBeenCalledTimes(2)
+ expect(mocks.oauth).not.toHaveBeenCalled()
+ })
+
+ it('fails before mutation when direct validation also exceeds its deadline', async () => {
+ const listing = new AbortController()
+ const details = new AbortController()
+ vi.spyOn(AbortSignal, 'timeout')
+ .mockReturnValueOnce(listing.signal)
+ .mockReturnValueOnce(details.signal)
+ mocks.selector
+ .mockResolvedValueOnce({ kind: 'list', items: [] })
+ .mockImplementationOnce(({ input }: ValidationSelectorCall) => {
+ details.abort(new DOMException('Validation timed out', 'TimeoutError'))
+ input.signal.throwIfAborted()
+ })
+ await expect(runConnect()).rejects.toThrow('took too long')
+ expect(mocks.configure).not.toHaveBeenCalled()
+ })
+
+ it('does not retry direct validation after the caller cancels listing', async () => {
+ const caller = new AbortController()
+ mocks.selector.mockImplementationOnce(({ input }: ValidationSelectorCall) => {
+ caller.abort(new Error('Setup cancelled'))
+ input.signal.throwIfAborted()
+ })
+ await expect(
+ personalSourceSetup.execute({
+ principal,
+ input: { ...connect, keys: ['PROJECT'] },
+ request: { headers: new Headers(), signal: caller.signal },
+ })
+ ).rejects.toThrow('Setup cancelled')
+ expect(mocks.selector).toHaveBeenCalledTimes(1)
+ expect(mocks.configure).not.toHaveBeenCalled()
+ })
+
+ it('rejects a detail response for a different key', async () => {
+ mocks.selector
+ .mockResolvedValueOnce({ kind: 'list', items: [] })
+ .mockResolvedValueOnce({ kind: 'detail', item: { id: 'OTHER', label: 'Other project' } })
+ await expect(runConnect()).rejects.toThrow('could not be found')
+ expect(mocks.configure).not.toHaveBeenCalled()
+ })
+
+ it('bounds direct validation concurrency and validates each unresolved key only once', async () => {
+ let release = () => {}
+ let allStarted = () => {}
+ const gate = new Promise((resolve) => {
+ release = resolve
+ })
+ const started = new Promise((resolve) => {
+ allStarted = resolve
+ })
+ let active = 0
+ let maximumActive = 0
+ mocks.selector.mockImplementation(async ({ input }: ValidationSelectorCall) => {
+ if (input.request.kind === 'list') return { kind: 'list', items: [] }
+ active++
+ maximumActive = Math.max(maximumActive, active)
+ if (active === 5) allStarted()
+ await gate
+ active--
+ return { kind: 'detail', item: { id: input.request.id, label: input.request.id } }
+ })
+ const keys = Array.from({ length: 12 }, (_, index) => `PROJECT${index}`)
+ const connection = runConnect({ keys: [...keys, ...keys] })
+ await started
+ try {
+ expect(mocks.selector).toHaveBeenCalledTimes(6)
+ expect(mocks.configure).not.toHaveBeenCalled()
+ } finally {
+ release()
+ }
+ await expect(connection).resolves.toMatchObject({ kind: 'connected' })
+ expect(maximumActive).toBe(5)
+ expect(mocks.selector).toHaveBeenCalledTimes(keys.length + 1)
+ })
+
+ it('propagates listing failures without starting direct validation', async () => {
+ mocks.selector.mockRejectedValueOnce(new Error('Account revoked'))
+ await expect(runConnect()).rejects.toThrow('Account revoked')
+ expect(mocks.selector).toHaveBeenCalledTimes(1)
+ expect(mocks.configure).not.toHaveBeenCalled()
+ })
+
+ it('rejects stale or wrong-user credentials before provider calls', async () => {
+ mocks.ownAccount.mockRejectedValue(new Error('Account unavailable'))
+ await expect(runConnect()).rejects.toThrow('Account unavailable')
+ expect(mocks.selector).not.toHaveBeenCalled()
+ expect(mocks.configure).not.toHaveBeenCalled()
+ })
+
+ it('rejects an account whose enrollment group differs from the organization setup', async () => {
+ mocks.binding.mockResolvedValue({
+ organizationId: owner.organizationId,
+ credentialGroupId: 'other-group',
+ credentialGroupOptionId: 'option-1',
+ })
+ await expect(runConnect()).rejects.toThrow('Connect your account again')
+ expect(mocks.configure).not.toHaveBeenCalled()
+ })
+
+ it.each(['binding', 'ownership'] as const)(
+ 'stops before source creation if the caller cancels during the final %s check',
+ async (phase) => {
+ const caller = new AbortController()
+ if (phase === 'binding') {
+ mocks.binding.mockImplementationOnce(() => {
+ caller.abort(new Error('Setup cancelled'))
+ return {
+ organizationId: owner.organizationId,
+ credentialGroupId: 'group-1',
+ credentialGroupOptionId: 'option-1',
+ }
+ })
+ } else {
+ mocks.ownAccount.mockResolvedValueOnce(account).mockImplementationOnce(() => {
+ caller.abort(new Error('Setup cancelled'))
+ return account
+ })
+ }
+ await expect(
+ personalSourceSetup.execute({
+ principal,
+ input: { ...connect, keys: ['PROJECT'] },
+ request: { headers: new Headers(), signal: caller.signal },
+ })
+ ).rejects.toThrow('Setup cancelled')
+ expect(mocks.configure).not.toHaveBeenCalled()
+ expect(mocks.dispatch).not.toHaveBeenCalled()
+ }
+ )
+
+ it('rejects a current operation authorization denial before any setup effects', async () => {
+ mocks.authorizeOperation.mockRejectedValue(new Error('Membership ended'))
+ await expect(runConnect()).rejects.toThrow('Membership ended')
+ expect(mocks.authorize).not.toHaveBeenCalled()
+ expect(mocks.configure).not.toHaveBeenCalled()
+ })
+
+ it.each([
+ 'example.atlassian.net/wiki/spaces',
+ 'user:password@example.atlassian.net',
+ 'example.atlassian.net?site=other',
+ ])('rejects an invalid site value before discovery: %s', async (domain) => {
+ await expect(runConnect({ domain })).rejects.toThrow('hostname')
+ expect(mocks.selector).not.toHaveBeenCalled()
+ })
+
+ it('keeps the configured source available if the best-effort initial dispatch fails', async () => {
+ mocks.dispatch.mockRejectedValue(new Error('Queue temporarily unavailable'))
+ await expect(runConnect()).resolves.toMatchObject({
+ kind: 'connected',
+ connectorId: 'source-1',
+ })
+ })
+})
diff --git a/apps/sim/lib/knowledge/application/personal-source-setup.ts b/apps/sim/lib/knowledge/application/personal-source-setup.ts
new file mode 100644
index 00000000000..407ad85b918
--- /dev/null
+++ b/apps/sim/lib/knowledge/application/personal-source-setup.ts
@@ -0,0 +1,298 @@
+import { createLogger } from '@sim/logger'
+import { getErrorMessage } from '@sim/utils/errors'
+import { normalizeAtlassianSiteUrl } from '@/lib/atlassian/discovery'
+import { OrchestrationError } from '@/lib/core/orchestration/types'
+import { mapWithConcurrency } from '@/lib/core/utils/concurrency'
+import {
+ loadManagedCredentialGroupBinding,
+ loadScopedAccountsCredentialListContext,
+} from '@/lib/credential-groups/credentials'
+import { getCredentialGroupOAuthContextForEnrollment } from '@/lib/credential-groups/enrollments'
+import { startCredentialGroupOAuth } from '@/lib/credential-groups/oauth'
+import { readSearchConnectionCompletion } from '@/lib/credential-groups/search-connection-completion'
+import { createViewerCredentialGroupEnrollment } from '@/lib/credential-groups/self-enrollment'
+import { getOwnOrganizationManagedOAuthCredentials } from '@/lib/credentials/organization-managed'
+import { defineAuthorizedKnowledgeUseCase } from '@/lib/knowledge/application/authorized-knowledge-use-case'
+import { resolveKnowledgeBillingAttribution } from '@/lib/knowledge/application/billing'
+import { resolveKnowledgeOrganizationContext } from '@/lib/knowledge/application/contexts'
+import { knowledgeOperations } from '@/lib/knowledge/application/operations'
+import {
+ authorizePersonalSearchSetup,
+ authorizePersonalSearchSetupCredential,
+ type PersonalSearchSetupConnector,
+} from '@/lib/knowledge/application/personal-search-account'
+import { configureSimSearchConnector } from '@/lib/knowledge/application/sim-search'
+import { provisionKnowledgeConnectorMembersBinding } from '@/lib/knowledge/connectors/member-provisioning'
+import { executeSelector } from '@/lib/selectors/application/execute-selector'
+import { MAX_SELECTOR_PAGES } from '@/lib/selectors/limits'
+import type { SelectorExecutionResult, SelectorRequest } from '@/lib/selectors/types'
+import { MAX_PERSONAL_SOURCE_SETUP_KEYS } from '@/lib/sim-search/personal-source-setup'
+import { CONNECTOR_META_REGISTRY } from '@/connectors/registry'
+
+const logger = createLogger('PersonalSourceSetup')
+const VALIDATION_PHASE_TIMEOUT_MS = 30_000
+const DETAIL_VALIDATION_CONCURRENCY = 5
+
+interface PersonalSourceSetupOwner {
+ organizationId: string
+ connectorType: PersonalSearchSetupConnector
+}
+
+type PersonalSourceSetupInput = PersonalSourceSetupOwner &
+ (
+ | { action: 'authorize'; oauthCompletionId: string }
+ | { action: 'options'; credentialId: string; domain: string; request: SelectorRequest }
+ | { action: 'connect'; credentialId: string; domain: string; keys: string[] }
+ )
+
+type PersonalSourceSetupResult =
+ | { kind: 'authorization'; url: string }
+ | { kind: 'connected'; knowledgeBaseId: string; connectorId: string }
+ | SelectorExecutionResult
+
+function normalizeSetupDomain(value: string) {
+ let url: URL
+ try {
+ url = new URL(normalizeAtlassianSiteUrl(value))
+ } catch {
+ throw new OrchestrationError('validation', 'Enter your Atlassian site hostname')
+ }
+ if (
+ url.username ||
+ url.password ||
+ url.port ||
+ url.pathname !== '/' ||
+ url.search ||
+ url.hash ||
+ !url.hostname.includes('.')
+ ) {
+ throw new OrchestrationError(
+ 'validation',
+ 'Enter your Atlassian site hostname without a page path'
+ )
+ }
+ return url.hostname
+}
+
+/** Lists the viewer's live accounts independently of whether any source exists yet. */
+export const listPersonalSourceSetupAccounts = defineAuthorizedKnowledgeUseCase({
+ operation: knowledgeOperations.listPersonalSourceSetupAccounts,
+ resolveContext: ({ input }: { input: PersonalSourceSetupOwner & { completionId?: string } }) =>
+ resolveKnowledgeOrganizationContext(input),
+ async execute({ principal, input }) {
+ const userId = await authorizePersonalSearchSetup(principal, input)
+ const accounts = await getOwnOrganizationManagedOAuthCredentials({
+ organizationId: input.organizationId,
+ userId,
+ providerId: input.connectorType,
+ })
+ const completedCredentialId = input.completionId
+ ? await readSearchConnectionCompletion({
+ organizationId: input.organizationId,
+ userId,
+ completionId: input.completionId,
+ })
+ : null
+ return {
+ accounts: accounts.map((account) => ({
+ id: account.id,
+ name: account.displayName,
+ provider: input.connectorType,
+ type: 'managed_oauth' as const,
+ scopes: account.scopes,
+ })),
+ completedCredentialId: accounts.some((account) => account.id === completedCredentialId)
+ ? completedCredentialId
+ : null,
+ }
+ },
+})
+
+/** Connects the viewer first, then configures only an approved personal-account Search source. */
+export const personalSourceSetup = defineAuthorizedKnowledgeUseCase({
+ operation: knowledgeOperations.personalSourceSetup,
+ resolveContext: ({ input }: { input: PersonalSourceSetupInput }) =>
+ resolveKnowledgeOrganizationContext(input),
+ async execute({ principal, input, context, request }): Promise {
+ const userId = await authorizePersonalSearchSetup(principal, input)
+ if (input.action === 'authorize') {
+ const binding = await provisionKnowledgeConnectorMembersBinding({
+ organizationId: input.organizationId,
+ connectorMeta: CONNECTOR_META_REGISTRY[input.connectorType]!,
+ userId,
+ })
+ const { enrollment, invitationLink } = await createViewerCredentialGroupEnrollment({
+ organizationId: input.organizationId,
+ userId,
+ credentialGroupId: binding.credentialGroupId,
+ })
+ const token = new URL(invitationLink).pathname.split('/').at(-1)
+ if (!token) throw new Error('Account enrollment did not return an invitation token')
+ const oauth = await getCredentialGroupOAuthContextForEnrollment(
+ {
+ organizationId: input.organizationId,
+ credentialGroupId: binding.credentialGroupId,
+ enrollmentId: enrollment.id,
+ email: enrollment.email,
+ userId,
+ },
+ binding.credentialGroupOptionId
+ )
+ if (!oauth)
+ throw new OrchestrationError('forbidden', 'This account connection is no longer available')
+ return {
+ kind: 'authorization',
+ url: await startCredentialGroupOAuth(oauth, token, {
+ completionRedirect: true,
+ returnTo: 'search',
+ completionId: input.oauthCompletionId,
+ connectionIntent: { kind: 'create' },
+ }),
+ }
+ }
+
+ await authorizePersonalSearchSetupCredential(principal, input)
+ const domain = normalizeSetupDomain(input.domain)
+ const selectorInput = {
+ selectorKey:
+ input.connectorType === 'jira'
+ ? ('jira.projectKeys' as const)
+ : ('confluence.spaces' as const),
+ scope: { kind: 'organization' as const, organizationId: input.organizationId },
+ context: { oauthCredential: input.credentialId, domain },
+ personalSearchSetup: input.connectorType,
+ }
+ if (input.action === 'options') {
+ return executeSelector.execute({
+ principal,
+ request,
+ input: { ...selectorInput, request: input.request },
+ })
+ }
+ if (
+ !input.keys.length ||
+ input.keys.length > MAX_PERSONAL_SOURCE_SETUP_KEYS ||
+ input.keys.some((key) => !key.trim() || key.length > 255)
+ ) {
+ throw new OrchestrationError('validation', 'Select between 1 and 1,000 projects or spaces')
+ }
+ const keys = [...new Set(input.keys.map((key) => key.trim()))]
+ const remaining = new Set(keys)
+ const cursors = new Set()
+ const timeout = AbortSignal.timeout(VALIDATION_PHASE_TIMEOUT_MS)
+ const signal = request?.signal ? AbortSignal.any([request.signal, timeout]) : timeout
+ let cursor: string | undefined
+ for (let page = 0; page < MAX_SELECTOR_PAGES; page++) {
+ let result: SelectorExecutionResult
+ try {
+ signal.throwIfAborted()
+ result = await executeSelector.execute({
+ principal,
+ request,
+ input: {
+ ...selectorInput,
+ signal,
+ request: { kind: 'list', ...(cursor ? { cursor } : {}) },
+ },
+ })
+ signal.throwIfAborted()
+ } catch (error) {
+ if (timeout.aborted && !request?.signal?.aborted) break
+ throw error
+ }
+ if (result.kind !== 'list') throw new Error('Source discovery returned an unexpected result')
+ for (const option of result.items) remaining.delete(option.id)
+ if (remaining.size === 0) break
+ if (!result.nextCursor || result.truncated || cursors.has(result.nextCursor)) break
+ cursor = result.nextCursor
+ cursors.add(cursor)
+ }
+ request?.signal?.throwIfAborted()
+ if (remaining.size > 0) {
+ const detailTimeout = AbortSignal.timeout(VALIDATION_PHASE_TIMEOUT_MS)
+ const details = new AbortController()
+ const detailSignal = AbortSignal.any([
+ detailTimeout,
+ details.signal,
+ ...(request?.signal ? [request.signal] : []),
+ ])
+ try {
+ await mapWithConcurrency([...remaining], DETAIL_VALIDATION_CONCURRENCY, async (key) => {
+ detailSignal.throwIfAborted()
+ const result = await executeSelector.execute({
+ principal,
+ request,
+ input: {
+ ...selectorInput,
+ signal: detailSignal,
+ request: { kind: 'detail', id: key },
+ },
+ })
+ detailSignal.throwIfAborted()
+ if (result.kind !== 'detail' || result.item?.id !== key) {
+ throw new OrchestrationError(
+ 'validation',
+ 'Some selected projects or spaces could not be found with this account. Refresh the choices and try again.'
+ )
+ }
+ })
+ } catch (error) {
+ details.abort(error)
+ if (detailTimeout.aborted && !request?.signal?.aborted) {
+ throw new OrchestrationError(
+ 'validation',
+ 'Checking the selected projects or spaces took too long. Try fewer selections.'
+ )
+ }
+ throw error
+ }
+ }
+ const [binding, group] = await Promise.all([
+ loadManagedCredentialGroupBinding(input.credentialId),
+ loadScopedAccountsCredentialListContext({
+ kind: 'organization',
+ organizationId: input.organizationId,
+ }),
+ ])
+ if (
+ !binding ||
+ !group ||
+ binding.credentialGroupId !== group.credentialGroupId ||
+ binding.organizationId !== input.organizationId
+ ) {
+ throw new OrchestrationError(
+ 'not_found',
+ 'Connect your account again before choosing sources'
+ )
+ }
+ await authorizePersonalSearchSetupCredential(principal, input)
+ request?.signal?.throwIfAborted()
+ const result = await configureSimSearchConnector.execute({
+ principal,
+ request,
+ input: {
+ organizationId: input.organizationId,
+ connectorType: input.connectorType,
+ memberCredentialBinding: {
+ credentialGroupId: binding.credentialGroupId,
+ credentialGroupOptionId: binding.credentialGroupOptionId,
+ },
+ sourceConfig: {
+ domain,
+ [input.connectorType === 'jira' ? 'projectKey' : 'spaceKey']: [...keys].join(','),
+ },
+ },
+ })
+ try {
+ const { dispatchMemberSync } = await import('@/lib/knowledge/connectors/member-queue')
+ await dispatchMemberSync(result.connectorId, {
+ billingAttribution: await resolveKnowledgeBillingAttribution(principal, context),
+ })
+ } catch (error) {
+ logger.warn('Initial personal source sync will retry on its schedule', {
+ error: getErrorMessage(error),
+ })
+ }
+ return { kind: 'connected', ...result }
+ },
+})
diff --git a/apps/sim/lib/knowledge/application/search.test.ts b/apps/sim/lib/knowledge/application/search.test.ts
index abbb70f08f9..49aa6d88584 100644
--- a/apps/sim/lib/knowledge/application/search.test.ts
+++ b/apps/sim/lib/knowledge/application/search.test.ts
@@ -26,12 +26,17 @@ const mocks = vi.hoisted(() => ({
importProvenance: vi.fn(),
rerank: vi.fn(),
searched: vi.fn(),
+ recordActivity: vi.fn(),
}))
vi.mock('@/lib/core/telemetry', () => ({
PlatformEvents: { knowledgeBaseSearched: mocks.searched },
}))
+vi.mock('@/lib/knowledge/search/activity', () => ({
+ recordOrganizationSearchActivity: mocks.recordActivity,
+}))
+
vi.mock('@/lib/knowledge/reranker', () => ({
rerank: mocks.rerank,
}))
@@ -209,6 +214,23 @@ describe('knowledge search application use case', () => {
}
})
+ it('meters only successful organization calls under the acting person', async () => {
+ await searchKnowledge.execute({
+ principal: { kind: 'session', userId: 'user-1', sessionId: 'session-1' },
+ input: { knowledgeBaseIds: ['knowledge-1'], query: 'answer', topK: 10, surface: 'mcp' },
+ })
+ if (scope === 'organization') {
+ expect(mocks.recordActivity).toHaveBeenCalledExactlyOnceWith({
+ organizationId: 'org-canonical',
+ userId: 'user-1',
+ surface: 'mcp',
+ results: expect.any(Array),
+ })
+ } else {
+ expect(mocks.recordActivity).not.toHaveBeenCalled()
+ }
+ })
+
const principal = { kind: 'session', userId: 'user-1', sessionId: 'session-1' } as const
const input = { knowledgeBaseIds: ['knowledge-1'], query: 'answer', topK: 10 }
@@ -274,6 +296,7 @@ describe('knowledge search application use case', () => {
input: { knowledgeBaseIds: ['knowledge-1'], query: 'answer', topK: 5 },
})
).rejects.toThrow('Search is not enabled for this organization')
+ expect(mocks.recordActivity).not.toHaveBeenCalled()
expect(mocks.requireOrganizationSearch).toHaveBeenCalledExactlyOnceWith('org-canonical')
expect(mocks.resolveBilling).not.toHaveBeenCalled()
expect(mocks.generateEmbedding).not.toHaveBeenCalled()
diff --git a/apps/sim/lib/knowledge/application/search.ts b/apps/sim/lib/knowledge/application/search.ts
index 7e1d6d0da95..9568e817c9d 100644
--- a/apps/sim/lib/knowledge/application/search.ts
+++ b/apps/sim/lib/knowledge/application/search.ts
@@ -39,6 +39,7 @@ import { generateSearchEmbedding, type KbEmbeddingTarget } from '@/lib/knowledge
import { runWithKnowledgeModelInputProvenance } from '@/lib/knowledge/model-input-provenance'
import { rerank } from '@/lib/knowledge/reranker'
import type { RerankerStatus } from '@/lib/knowledge/reranker-models'
+import { recordOrganizationSearchActivity } from '@/lib/knowledge/search/activity'
import { resolveKnowledgeSearchDefaults } from '@/lib/knowledge/search/defaults'
import type { WorkspaceSearchFilters } from '@/lib/knowledge/search/filters'
import {
@@ -687,7 +688,16 @@ export const searchKnowledge = defineAuthorizedKnowledgeUseCase({
resultSecretRegistry: registry,
}
},
- afterSuccess: ({ principal, context, input, result }) => {
+ afterSuccess: async ({ principal, context, input, result }) => {
+ const actorUserId = resolvePrincipalSubjectUserId(principal)
+ if (context.organizationId && actorUserId) {
+ await recordOrganizationSearchActivity({
+ organizationId: context.organizationId,
+ userId: actorUserId,
+ surface: input.surface ?? 'other',
+ results: result.results,
+ })
+ }
PlatformEvents.knowledgeBaseSearched({
knowledgeBaseId: result.knowledgeBaseId,
knowledgeBaseIds: result.knowledgeBaseIds,
diff --git a/apps/sim/lib/knowledge/application/sim-search.test.ts b/apps/sim/lib/knowledge/application/sim-search.test.ts
index 78e2fbf60c7..fd1b47aa7e0 100644
--- a/apps/sim/lib/knowledge/application/sim-search.test.ts
+++ b/apps/sim/lib/knowledge/application/sim-search.test.ts
@@ -145,6 +145,7 @@ vi.mock('@/connectors/registry', () => ({
}))
import {
+ configureSimSearchConnector,
connectSimSearchConnector,
prepareSearchSource,
} from '@/lib/knowledge/application/sim-search'
@@ -202,6 +203,53 @@ describe('connectSimSearchConnector', () => {
)
})
+ it('reuses a prepared account only when the source enrollment group and option match', async () => {
+ mocks.resolvePermission.mockResolvedValue('read')
+ queueTableRows(knowledgeConnector, [
+ { ...existingConnector, credentialGroupId: 'group-1', credentialGroupOptionId: 'option-1' },
+ ])
+ await expect(
+ configureSimSearchConnector.execute({
+ principal,
+ input: {
+ workspaceId: 'workspace-1',
+ connectorType: 'google_drive',
+ memberCredentialBinding: {
+ credentialGroupId: 'group-1',
+ credentialGroupOptionId: 'option-1',
+ },
+ },
+ })
+ ).resolves.toEqual(existingConnector)
+ expect(mocks.enroll).not.toHaveBeenCalled()
+ })
+
+ it.each([
+ { credentialGroupId: 'other-group', credentialGroupOptionId: 'option-1' },
+ { credentialGroupId: 'group-1', credentialGroupOptionId: 'other-option' },
+ ])(
+ 'refuses an existing source with a mismatched prepared account binding %#',
+ async (sourceBinding) => {
+ mocks.resolvePermission.mockResolvedValue('read')
+ queueTableRows(knowledgeConnector, [{ ...existingConnector, ...sourceBinding }])
+ await expect(
+ configureSimSearchConnector.execute({
+ principal,
+ input: {
+ workspaceId: 'workspace-1',
+ connectorType: 'google_drive',
+ memberCredentialBinding: {
+ credentialGroupId: 'group-1',
+ credentialGroupOptionId: 'option-1',
+ },
+ },
+ })
+ ).rejects.toMatchObject({ code: 'conflict' })
+ expect(mocks.enroll).not.toHaveBeenCalled()
+ expect(mocks.createConnector).not.toHaveBeenCalled()
+ }
+ )
+
it('prepares a supported administrative source in the existing workspace index', async () => {
mocks.resolvePermission.mockResolvedValue('admin')
queueTableRows(knowledgeBase, [{ id: 'kb-existing' }])
diff --git a/apps/sim/lib/knowledge/application/sim-search.ts b/apps/sim/lib/knowledge/application/sim-search.ts
index 92367573822..94508a94d9b 100644
--- a/apps/sim/lib/knowledge/application/sim-search.ts
+++ b/apps/sim/lib/knowledge/application/sim-search.ts
@@ -69,6 +69,8 @@ export interface ConnectSimSearchConnectorInput extends ResourceOwner {
/** Correlates a direct provider authorization with the initiating Integrations tab. */
connectionIntent?: CredentialGroupConnectionIntent
oauthCompletionId?: string
+ /** Internal setup assertion; a reused source must use the viewer's prepared enrollment option. */
+ memberCredentialBinding?: { credentialGroupId: string; credentialGroupOptionId: string }
}
export interface ConnectSimSearchConnectorResult {
@@ -84,6 +86,8 @@ async function findSimSearchConnector(input: ConnectSimSearchConnectorInput) {
knowledgeBaseId: knowledgeBase.id,
connectorId: knowledgeConnector.id,
sourceConfig: knowledgeConnector.sourceConfig,
+ credentialGroupId: knowledgeConnector.credentialGroupId,
+ credentialGroupOptionId: knowledgeConnector.credentialGroupOptionId,
})
.from(knowledgeConnector)
.innerJoin(knowledgeBase, eq(knowledgeBase.id, knowledgeConnector.knowledgeBaseId))
@@ -119,6 +123,17 @@ async function findSimSearchConnector(input: ConnectSimSearchConnectorInput) {
)
}
const match = matches[0]
+ if (
+ match &&
+ input.memberCredentialBinding &&
+ (match.credentialGroupId !== input.memberCredentialBinding.credentialGroupId ||
+ match.credentialGroupOptionId !== input.memberCredentialBinding.credentialGroupOptionId)
+ ) {
+ throw new OrchestrationError(
+ 'conflict',
+ 'This source uses a different account configuration. Ask an admin to review its settings.'
+ )
+ }
return match ? { knowledgeBaseId: match.knowledgeBaseId, connectorId: match.connectorId } : null
}
@@ -282,11 +297,11 @@ async function requireSimSearchSetupAdmin(
* singleflight also coalesces repeated setup clicks for each source; concurrent
* source creation is serialized by the connector insert transaction before enrollment.
*/
-export const connectSimSearchConnector = defineAuthorizedKnowledgeUseCase({
+export const configureSimSearchConnector = defineAuthorizedKnowledgeUseCase({
operation: knowledgeOperations.simSearchConnect,
resolveContext: ({ input }: { input: ConnectSimSearchConnectorInput }) =>
resolveKnowledgeOwnerContext(input),
- async execute({ principal, input, context, request }): Promise {
+ async execute({ principal, input, context, request }) {
const meta = CONNECTOR_META_REGISTRY[input.connectorType]
if (!meta || !canConnectPersonally(meta)) {
throw new OrchestrationError(
@@ -373,12 +388,23 @@ export const connectSimSearchConnector = defineAuthorizedKnowledgeUseCase({
}
)
}
+ return target
+ },
+})
+
+/** Creates or reuses the source, then authorizes the member when setup has not already done so. */
+export const connectSimSearchConnector = defineAuthorizedKnowledgeUseCase({
+ operation: knowledgeOperations.simSearchConnect,
+ resolveContext: ({ input }: { input: ConnectSimSearchConnectorInput }) =>
+ resolveKnowledgeOwnerContext(input),
+ async execute({ principal, input, context, request }): Promise {
+ const target = await configureSimSearchConnector.execute({ principal, input, request })
const { url } = await startKnowledgeConnectorMemberEnrollment.execute({
principal,
input: {
knowledgeBaseId: target.knowledgeBaseId,
connectorId: target.connectorId,
- assertedWorkspaceId: workspaceId,
+ assertedWorkspaceId: context.workspaceId,
assertedOrganizationId: context.organizationId,
oauthCompletionId: input.oauthCompletionId,
connectionIntent: input.connectionIntent,
diff --git a/apps/sim/lib/knowledge/application/slack-search/assistant.test.ts b/apps/sim/lib/knowledge/application/slack-search/assistant.test.ts
index d2f335c3e08..c57bb19758d 100644
--- a/apps/sim/lib/knowledge/application/slack-search/assistant.test.ts
+++ b/apps/sim/lib/knowledge/application/slack-search/assistant.test.ts
@@ -258,6 +258,7 @@ describe('organization Assistant from Slack', () => {
chatId: 'chat1',
goRoute: '/api/mothership',
autoExecuteTools: true,
+ searchSurface: 'slack',
billingAttribution: { organizationId: 'org1', actorUserId: 'member1' },
})
)
diff --git a/apps/sim/lib/knowledge/application/slack-search/assistant.ts b/apps/sim/lib/knowledge/application/slack-search/assistant.ts
index 552a68ac710..297d7cdb25d 100644
--- a/apps/sim/lib/knowledge/application/slack-search/assistant.ts
+++ b/apps/sim/lib/knowledge/application/slack-search/assistant.ts
@@ -255,6 +255,7 @@ export async function runSlackSearchAssistant(
abortSignal: controller.signal,
timeout: SLACK_SEARCH_MAX_DURATION_SECONDS * 1000,
autoExecuteTools: true,
+ searchSurface: 'slack',
onEvent: async (event) => {
try {
await responseStream.onEvent(event)
diff --git a/apps/sim/lib/knowledge/application/workspace-search.activity.test.ts b/apps/sim/lib/knowledge/application/workspace-search.activity.test.ts
new file mode 100644
index 00000000000..32ecacf0059
--- /dev/null
+++ b/apps/sim/lib/knowledge/application/workspace-search.activity.test.ts
@@ -0,0 +1,144 @@
+/** @vitest-environment node */
+import { member } from '@sim/db/schema'
+import { queueTableRows, resetDbChainMock } from '@sim/testing'
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+
+const mocks = vi.hoisted(() => ({
+ context: vi.fn(),
+ policy: vi.fn(),
+ findIndex: vi.fn(),
+ available: vi.fn(),
+ activity: vi.fn(),
+ search: vi.fn(),
+}))
+vi.mock('@/lib/knowledge/application/contexts', () => ({
+ resolveKnowledgeOrganizationContext: mocks.context,
+ resolveKnowledgeOwnerContext: mocks.context,
+ resolveKnowledgeWorkspaceContext: mocks.context,
+}))
+vi.mock('@/lib/permission-groups/resolve.server', () => ({
+ getUserPermissionConfigForOrganization: mocks.policy,
+}))
+vi.mock('@sim/platform-authz/workspace', () => ({
+ isOrgAdminRole: (role: string) => role === 'admin' || role === 'owner',
+}))
+vi.mock('@/lib/knowledge/search/search-index', () => ({
+ findSearchIndex: mocks.findIndex,
+ findWorkspaceSearchIndex: mocks.findIndex,
+}))
+vi.mock('@/lib/knowledge/access/availability', () => ({
+ requireOrganizationSearchAvailable: mocks.available,
+}))
+vi.mock('@/lib/knowledge/search/activity', () => ({
+ recordOrganizationSearchActivity: mocks.activity,
+}))
+vi.mock('@/lib/knowledge/application/search', () => ({
+ searchKnowledge: { execute: mocks.search },
+}))
+
+import {
+ searchOrganizationKnowledge,
+ searchScopedKnowledge,
+} from '@/lib/knowledge/application/workspace-search'
+
+const principal = { kind: 'session', userId: 'reader', sessionId: 'session' } as const
+const input = { organizationId: 'org', query: 'policy', topK: 20, surface: 'slack' } as const
+
+beforeEach(() => {
+ vi.clearAllMocks()
+ resetDbChainMock()
+ mocks.context.mockResolvedValue({ organizationId: 'org' })
+ mocks.policy.mockResolvedValue(null)
+ mocks.findIndex.mockResolvedValue(null)
+ mocks.available.mockResolvedValue(undefined)
+ mocks.activity.mockResolvedValue(undefined)
+ mocks.search.mockResolvedValue({ results: [], knowledgeBases: [{ id: 'index' }] })
+})
+
+describe.each([
+ { name: 'organization Assistant', operation: searchOrganizationKnowledge },
+ { name: 'scoped Search', operation: searchScopedKnowledge },
+])('$name activity before an index exists', ({ operation }) => {
+ it('records an authorized empty invocation for the acting member', async () => {
+ queueTableRows(member, [{ role: 'member' }])
+ expect(await operation.execute({ principal, input })).toEqual({
+ results: [],
+ query: 'policy',
+ knowledgeBases: [],
+ })
+ expect(mocks.available).toHaveBeenCalledExactlyOnceWith('org')
+ expect(mocks.activity).toHaveBeenCalledExactlyOnceWith({
+ organizationId: 'org',
+ userId: 'reader',
+ surface: 'slack',
+ results: [],
+ })
+ expect(mocks.search).not.toHaveBeenCalled()
+ })
+
+ it('leaves indexed invocation metering to the canonical search operation', async () => {
+ queueTableRows(member, [{ role: 'member' }])
+ mocks.findIndex.mockResolvedValueOnce({ id: 'index' })
+ await operation.execute({ principal, input })
+ expect(mocks.search).toHaveBeenCalledOnce()
+ expect(mocks.search).toHaveBeenCalledWith({
+ principal,
+ input: expect.objectContaining({ knowledgeBaseIds: ['index'], surface: 'slack' }),
+ })
+ expect(mocks.activity).not.toHaveBeenCalled()
+ })
+
+ it('does not meter an unavailable Search request', async () => {
+ queueTableRows(member, [{ role: 'member' }])
+ mocks.available.mockRejectedValueOnce(new Error('Search is disabled'))
+ await expect(operation.execute({ principal, input })).rejects.toThrow('Search is disabled')
+ expect(mocks.activity).not.toHaveBeenCalled()
+ expect(mocks.search).not.toHaveBeenCalled()
+ })
+
+ it('does not discover the index or meter a nonmember request', async () => {
+ queueTableRows(member, [])
+ await expect(operation.execute({ principal, input })).rejects.toMatchObject({
+ code: 'not_found',
+ })
+ expect(mocks.findIndex).not.toHaveBeenCalled()
+ expect(mocks.activity).not.toHaveBeenCalled()
+ })
+
+ it('does not meter a request that was already cancelled', async () => {
+ queueTableRows(member, [{ role: 'member' }])
+ const controller = new AbortController()
+ controller.abort(new Error('Search cancelled'))
+ await expect(
+ operation.execute({ principal, input: { ...input, signal: controller.signal } })
+ ).rejects.toThrow('Search cancelled')
+ expect(mocks.activity).not.toHaveBeenCalled()
+ expect(mocks.search).not.toHaveBeenCalled()
+ })
+
+ it.each(['index', 'availability'] as const)(
+ 'does not meter a request cancelled during the %s lookup',
+ async (lookup) => {
+ queueTableRows(member, [{ role: 'member' }])
+ const controller = new AbortController()
+ const cancel = () => controller.abort(new Error('Search cancelled'))
+ if (lookup === 'index') {
+ mocks.findIndex.mockImplementationOnce(async () => {
+ cancel()
+ return null
+ })
+ } else {
+ mocks.available.mockImplementationOnce(async () => {
+ cancel()
+ })
+ }
+
+ await expect(
+ operation.execute({ principal, input: { ...input, signal: controller.signal } })
+ ).rejects.toThrow('Search cancelled')
+ expect(mocks.findIndex).toHaveBeenCalledOnce()
+ expect(mocks.activity).not.toHaveBeenCalled()
+ expect(mocks.search).not.toHaveBeenCalled()
+ }
+ )
+})
diff --git a/apps/sim/lib/knowledge/application/workspace-search.ts b/apps/sim/lib/knowledge/application/workspace-search.ts
index 67598b11c7d..4c44f62cf0e 100644
--- a/apps/sim/lib/knowledge/application/workspace-search.ts
+++ b/apps/sim/lib/knowledge/application/workspace-search.ts
@@ -1,4 +1,6 @@
+import { resolvePrincipalSubjectUserId } from '@sim/auth/principal'
import { type ResourceOwner, resourceScopeFromOwner } from '@/lib/core/resource-scope'
+import { requireOrganizationSearchAvailable } from '@/lib/knowledge/access/availability'
import { defineAuthorizedKnowledgeUseCase } from '@/lib/knowledge/application/authorized-knowledge-use-case'
import {
resolveKnowledgeOrganizationContext,
@@ -7,6 +9,7 @@ import {
} from '@/lib/knowledge/application/contexts'
import { knowledgeOperations } from '@/lib/knowledge/application/operations'
import { type SearchKnowledgeInput, searchKnowledge } from '@/lib/knowledge/application/search'
+import { recordOrganizationSearchActivity } from '@/lib/knowledge/search/activity'
import { findSearchIndex, findWorkspaceSearchIndex } from '@/lib/knowledge/search/search-index'
export type SearchWorkspaceKnowledgeInput = Omit<
@@ -22,6 +25,7 @@ export const searchWorkspaceKnowledge = defineAuthorizedKnowledgeUseCase({
resolveContext: ({ input }: { input: SearchWorkspaceKnowledgeInput }) =>
resolveKnowledgeWorkspaceContext(input),
async execute({ principal, input, context }) {
+ input.signal?.throwIfAborted()
const index = await findWorkspaceSearchIndex(context.workspaceId)
if (!index) return { results: [], query: input.query ?? '', knowledgeBases: [] }
return searchKnowledge.execute({
@@ -42,11 +46,26 @@ export const searchOrganizationKnowledge = defineAuthorizedKnowledgeUseCase({
resolveContext: ({ input }: { input: SearchOrganizationKnowledgeInput }) =>
resolveKnowledgeOrganizationContext(input),
async execute({ principal, input, context }) {
+ input.signal?.throwIfAborted()
const index = await findSearchIndex({
kind: 'organization',
organizationId: context.organizationId,
})
- if (!index) return { results: [], query: input.query ?? '', knowledgeBases: [] }
+ if (!index) {
+ if (context.organizationId) {
+ await requireOrganizationSearchAvailable(context.organizationId)
+ input.signal?.throwIfAborted()
+ const userId = resolvePrincipalSubjectUserId(principal)
+ if (userId)
+ await recordOrganizationSearchActivity({
+ organizationId: context.organizationId,
+ userId,
+ surface: input.surface ?? 'other',
+ results: [],
+ })
+ }
+ return { results: [], query: input.query ?? '', knowledgeBases: [] }
+ }
return searchKnowledge.execute({ principal, input: { ...input, knowledgeBaseIds: [index.id] } })
},
})
@@ -63,8 +82,23 @@ export const searchScopedKnowledge = defineAuthorizedKnowledgeUseCase({
resolveContext: ({ input }: { input: SearchScopedKnowledgeInput }) =>
resolveKnowledgeOwnerContext(input),
async execute({ principal, input, context }) {
+ input.signal?.throwIfAborted()
const index = await findSearchIndex(resourceScopeFromOwner(context))
- if (!index) return { results: [], query: input.query ?? '', knowledgeBases: [] }
+ if (!index) {
+ if (context.organizationId) {
+ await requireOrganizationSearchAvailable(context.organizationId)
+ input.signal?.throwIfAborted()
+ const userId = resolvePrincipalSubjectUserId(principal)
+ if (userId)
+ await recordOrganizationSearchActivity({
+ organizationId: context.organizationId,
+ userId,
+ surface: input.surface ?? 'other',
+ results: [],
+ })
+ }
+ return { results: [], query: input.query ?? '', knowledgeBases: [] }
+ }
return searchKnowledge.execute({
principal,
input: {
diff --git a/apps/sim/lib/knowledge/search/activity-stats.ts b/apps/sim/lib/knowledge/search/activity-stats.ts
new file mode 100644
index 00000000000..29108baa858
--- /dev/null
+++ b/apps/sim/lib/knowledge/search/activity-stats.ts
@@ -0,0 +1,100 @@
+import { dbReplica } from '@sim/db'
+import { organizationSearchInvocation, user } from '@sim/db/schema'
+import { sql } from 'drizzle-orm'
+import {
+ getSearchStatsWindow,
+ SEARCH_STATS_PEOPLE_LIMIT,
+ type SEARCH_STATS_PERIODS,
+ SEARCH_STATS_SOURCE_LIMIT,
+ type SEARCH_STATS_SURFACES,
+ type SearchStatsDateRange,
+} from '@/lib/knowledge/search/stats'
+
+export interface SearchStatsInput extends SearchStatsDateRange {
+ organizationId: string
+ period: (typeof SEARCH_STATS_PERIODS)[number]
+ surface?: (typeof SEARCH_STATS_SURFACES)[number]
+}
+
+type StatsRow = {
+ totals: { invocations: number; activePeople: number; results: number }
+ series: { timestamp: string; invocations: number }[]
+ surfaces: { surface: (typeof SEARCH_STATS_SURFACES)[number]; invocations: number }[]
+ sources: { sourceType: string; invocations: number }[]
+ people: {
+ userId: string | null
+ name: string | null
+ email: string | null
+ invocations: number
+ sourceTypes: string[]
+ }[]
+}
+
+/** One statement snapshot; only bounded aggregates leave Postgres, never individual search records. */
+export async function loadOrganizationSearchStats(input: SearchStatsInput, now = new Date()) {
+ const { start, end, days } = getSearchStatsWindow(input.period, now, input)
+ const rows = await dbReplica.transaction(
+ async (tx) => {
+ await tx.execute(sql`SET LOCAL statement_timeout = '10s'`)
+ return tx.execute(sql`
+ WITH activity AS (
+ SELECT id, user_id, surface, source_types, result_count, created_at
+ FROM ${organizationSearchInvocation}
+ WHERE organization_id = ${input.organizationId}
+ AND created_at >= ${start.toISOString()}::timestamptz
+ AND created_at < ${end.toISOString()}::timestamptz
+ ${input.surface ? sql`AND surface = ${input.surface}` : sql``}
+ ), people AS (
+ SELECT user_id, count(*)::float8 AS invocations
+ FROM activity GROUP BY user_id
+ ORDER BY invocations DESC, user_id NULLS LAST
+ LIMIT ${SEARCH_STATS_PEOPLE_LIMIT}
+ )
+ SELECT
+ (SELECT jsonb_build_object(
+ 'invocations', count(*)::float8,
+ 'activePeople', count(DISTINCT user_id)::float8,
+ 'results', coalesce(sum(result_count), 0)::float8
+ ) FROM activity) AS totals,
+ (SELECT coalesce(jsonb_agg(row ORDER BY row.timestamp), '[]'::jsonb) FROM (
+ SELECT to_char(created_at AT TIME ZONE 'UTC', 'YYYY-MM-DD') || 'T00:00:00.000Z' AS timestamp,
+ count(*)::float8 AS invocations
+ FROM activity GROUP BY 1
+ ) row) AS series,
+ (SELECT coalesce(jsonb_agg(row ORDER BY row.invocations DESC, row.surface), '[]'::jsonb) FROM (
+ SELECT surface, count(*)::float8 AS invocations FROM activity GROUP BY surface
+ ) row) AS surfaces,
+ (SELECT coalesce(jsonb_agg(row ORDER BY row.invocations DESC, row."sourceType"), '[]'::jsonb) FROM (
+ SELECT source_type AS "sourceType", count(*)::float8 AS invocations
+ FROM activity CROSS JOIN LATERAL unnest(source_types) AS source_type
+ GROUP BY source_type ORDER BY invocations DESC, source_type
+ LIMIT ${SEARCH_STATS_SOURCE_LIMIT}
+ ) row) AS sources,
+ (SELECT coalesce(jsonb_agg(row ORDER BY row.invocations DESC, row."userId" NULLS LAST), '[]'::jsonb) FROM (
+ SELECT people.user_id AS "userId", ${user.name} AS name, ${user.email} AS email,
+ people.invocations,
+ ARRAY(
+ SELECT DISTINCT source_type FROM activity
+ CROSS JOIN LATERAL unnest(source_types) AS source_type
+ WHERE activity.user_id IS NOT DISTINCT FROM people.user_id
+ ORDER BY source_type LIMIT ${SEARCH_STATS_SOURCE_LIMIT}
+ ) AS "sourceTypes"
+ FROM people LEFT JOIN ${user} ON ${user.id} = people.user_id
+ ) row) AS people
+ `)
+ },
+ { accessMode: 'read only' }
+ )
+ const row = rows[0]
+ if (!row) throw new Error('Search activity aggregate returned no result')
+ const byDay = new Map(row.series.map((point) => [point.timestamp, point.invocations]))
+ return {
+ ...row,
+ start: start.toISOString(),
+ end: end.toISOString(),
+ series: Array.from({ length: days }, (_, index) => {
+ const timestamp = new Date(start.getTime() + index * 86_400_000).toISOString()
+ return { timestamp, invocations: byDay.get(timestamp) ?? 0 }
+ }),
+ }
+}
diff --git a/apps/sim/lib/knowledge/search/activity.test.ts b/apps/sim/lib/knowledge/search/activity.test.ts
new file mode 100644
index 00000000000..11eda26b4e9
--- /dev/null
+++ b/apps/sim/lib/knowledge/search/activity.test.ts
@@ -0,0 +1,98 @@
+/** @vitest-environment node */
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+
+const mocks = vi.hoisted(() => ({
+ values: vi.fn(),
+ insert: vi.fn(),
+ execute: vi.fn(),
+ transaction: vi.fn(),
+}))
+vi.mock('@sim/db', () => ({ db: { transaction: mocks.transaction } }))
+
+import { recordOrganizationSearchActivity } from '@/lib/knowledge/search/activity'
+
+beforeEach(() => {
+ vi.clearAllMocks()
+ mocks.insert.mockReturnValue({ values: mocks.values })
+ mocks.values.mockResolvedValue(undefined)
+ mocks.execute.mockResolvedValue(undefined)
+ mocks.transaction.mockImplementation((callback) =>
+ callback({ execute: mocks.execute, insert: mocks.insert })
+ )
+})
+
+describe('Search activity metering', () => {
+ it('counts documents once per invocation and stores no document identity or content', async () => {
+ await recordOrganizationSearchActivity({
+ organizationId: 'org',
+ userId: 'actor',
+ surface: 'mcp',
+ results: [
+ { documentId: 'private-document-1', connectorType: 'confluence' },
+ { documentId: 'private-document-1', connectorType: 'confluence' },
+ { documentId: 'private-document-2', connectorType: 'jira' },
+ { documentId: 'private-document-3', connectorType: null },
+ ],
+ })
+ expect(mocks.values).toHaveBeenCalledWith({
+ id: expect.any(String),
+ organizationId: 'org',
+ userId: 'actor',
+ surface: 'mcp',
+ sourceTypes: ['confluence', 'jira', 'uploads'],
+ resultCount: 3,
+ })
+ expect(JSON.stringify(mocks.values.mock.calls)).not.toContain('private-document')
+ })
+ it('records successful empty searches', async () => {
+ await recordOrganizationSearchActivity({
+ organizationId: 'org',
+ userId: 'actor',
+ surface: 'dashboard',
+ results: [],
+ })
+ expect(mocks.values).toHaveBeenCalledWith(
+ expect.objectContaining({ resultCount: 0, sourceTypes: [] })
+ )
+ })
+ it('applies a transaction-local statement deadline before inserting activity', async () => {
+ const timeout = Promise.withResolvers()
+ mocks.execute.mockReturnValueOnce(timeout.promise)
+ const recorded = recordOrganizationSearchActivity({
+ organizationId: 'org',
+ userId: 'actor',
+ surface: 'slack',
+ results: [],
+ })
+ expect(mocks.insert).not.toHaveBeenCalled()
+ expect(JSON.stringify(mocks.execute.mock.calls[0])).toContain(
+ "SET LOCAL statement_timeout = '2s'"
+ )
+ timeout.resolve()
+ await recorded
+ expect(mocks.insert).toHaveBeenCalledOnce()
+ })
+ it('does not attempt an unbounded insert if setting the deadline fails', async () => {
+ mocks.execute.mockRejectedValueOnce(new Error('Could not set statement timeout'))
+ await expect(
+ recordOrganizationSearchActivity({
+ organizationId: 'org',
+ userId: 'actor',
+ surface: 'slack',
+ results: [],
+ })
+ ).resolves.toBeUndefined()
+ expect(mocks.insert).not.toHaveBeenCalled()
+ })
+ it('does not fail Search when activity storage is unavailable', async () => {
+ mocks.values.mockRejectedValueOnce(new Error('offline'))
+ await expect(
+ recordOrganizationSearchActivity({
+ organizationId: 'org',
+ userId: 'actor',
+ surface: 'dashboard',
+ results: [],
+ })
+ ).resolves.toBeUndefined()
+ })
+})
diff --git a/apps/sim/lib/knowledge/search/activity.ts b/apps/sim/lib/knowledge/search/activity.ts
new file mode 100644
index 00000000000..f3a68393de7
--- /dev/null
+++ b/apps/sim/lib/knowledge/search/activity.ts
@@ -0,0 +1,38 @@
+import { db } from '@sim/db'
+import { organizationSearchInvocation } from '@sim/db/schema'
+import { createLogger } from '@sim/logger'
+import { getErrorMessage } from '@sim/utils/errors'
+import { generateId } from '@sim/utils/id'
+import { sql } from 'drizzle-orm'
+import type { SEARCH_STATS_SURFACES } from '@/lib/knowledge/search/stats'
+
+const logger = createLogger('OrganizationSearchActivity')
+
+interface SearchActivityInput {
+ organizationId: string
+ userId: string
+ surface: (typeof SEARCH_STATS_SURFACES)[number]
+ results: ReadonlyArray<{ documentId: string; connectorType: string | null }>
+}
+
+/** Records completed, authorized searches without query text, document IDs, or content. */
+export async function recordOrganizationSearchActivity(input: SearchActivityInput): Promise {
+ const sourceTypes = [...new Set(input.results.map((result) => result.connectorType ?? 'uploads'))]
+ try {
+ await db.transaction(async (tx) => {
+ await tx.execute(sql`SET LOCAL statement_timeout = '2s'`)
+ await tx.insert(organizationSearchInvocation).values({
+ id: generateId(),
+ organizationId: input.organizationId,
+ userId: input.userId,
+ surface: input.surface,
+ sourceTypes,
+ resultCount: new Set(input.results.map((result) => result.documentId)).size,
+ })
+ })
+ } catch (error) {
+ logger.warn('Failed to record organization Search activity', {
+ error: getErrorMessage(error),
+ })
+ }
+}
diff --git a/apps/sim/lib/knowledge/search/stats.test.ts b/apps/sim/lib/knowledge/search/stats.test.ts
new file mode 100644
index 00000000000..3e232bc97ab
--- /dev/null
+++ b/apps/sim/lib/knowledge/search/stats.test.ts
@@ -0,0 +1,97 @@
+/** @vitest-environment node */
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import { organizationSearchStatsQuerySchema } from '@/lib/api/contracts/knowledge/search-stats'
+import { getSearchStatsRangeError, getSearchStatsWindow } from '@/lib/knowledge/search/stats'
+
+const now = new Date('2026-09-10T20:00:00.000Z')
+beforeEach(() => {
+ vi.useFakeTimers()
+ vi.setSystemTime(now)
+})
+afterEach(() => vi.useRealTimers())
+
+describe('Search stats window and contract', () => {
+ it.each([
+ { period: 'today', start: '2026-09-10T00:00:00.000Z', days: 1 },
+ { period: '3d', start: '2026-09-08T00:00:00.000Z', days: 3 },
+ { period: '7d', start: '2026-09-04T00:00:00.000Z', days: 7 },
+ { period: '14d', start: '2026-08-28T00:00:00.000Z', days: 14 },
+ { period: '30d', start: '2026-08-12T00:00:00.000Z', days: 30 },
+ { period: '90d', start: '2026-06-13T00:00:00.000Z', days: 90 },
+ ] as const)('uses $days UTC calendar days including today', ({ period, start, days }) => {
+ const now = new Date('2026-09-10T20:00:00.000Z')
+ const window = getSearchStatsWindow(period, now)
+ expect(window).toEqual({ start: new Date(start), end: now, days })
+ })
+ it('includes the last custom day with an exclusive next-midnight bound', () => {
+ expect(
+ getSearchStatsWindow('custom', now, { startDate: '2026-08-31', endDate: '2026-09-02' })
+ ).toEqual({
+ start: new Date('2026-08-31T00:00:00.000Z'),
+ end: new Date('2026-09-03T00:00:00.000Z'),
+ days: 3,
+ })
+ expect(
+ getSearchStatsWindow('custom', now, { startDate: '2026-09-10', endDate: '2026-09-10' })
+ ).toEqual({
+ start: new Date('2026-09-10T00:00:00.000Z'),
+ end: now,
+ days: 1,
+ })
+ })
+ it.each([
+ { startDate: '2026-09-01' },
+ { endDate: '2026-09-01' },
+ { startDate: '2026-02-29', endDate: '2026-03-01' },
+ { startDate: '2026-04-31', endDate: '2026-05-01' },
+ { startDate: '2026-9-01', endDate: '2026-09-02' },
+ { startDate: '2026-09-03', endDate: '2026-09-02' },
+ { startDate: '2026-09-10', endDate: '2026-09-11' },
+ { startDate: '2026-06-12', endDate: '2026-09-10' },
+ ])('rejects invalid or unbounded custom ranges: %j', (range) => {
+ expect(getSearchStatsRangeError(range, now)).not.toBeNull()
+ expect(
+ organizationSearchStatsQuerySchema.safeParse({
+ organizationId: 'org',
+ period: 'custom',
+ ...range,
+ }).success
+ ).toBe(false)
+ expect(() => getSearchStatsWindow('custom', now, range)).toThrow()
+ })
+ it('accepts leap days and exactly 90 inclusive days, but rejects dates with a preset', () => {
+ for (const range of [
+ { startDate: '2024-02-29', endDate: '2024-03-01' },
+ { startDate: '2026-06-13', endDate: '2026-09-10' },
+ ]) {
+ expect(
+ organizationSearchStatsQuerySchema.safeParse({
+ organizationId: 'org',
+ period: 'custom',
+ ...range,
+ }).success
+ ).toBe(true)
+ expect(
+ organizationSearchStatsQuerySchema.safeParse({
+ organizationId: 'org',
+ period: '7d',
+ ...range,
+ }).success
+ ).toBe(false)
+ }
+ })
+ it('bounds scans and rejects caller-supplied surfaces', () => {
+ expect(
+ organizationSearchStatsQuerySchema.safeParse({ organizationId: 'org', period: '365d' })
+ .success
+ ).toBe(false)
+ expect(
+ organizationSearchStatsQuerySchema.safeParse({ organizationId: 'org', surface: 'forged' })
+ .success
+ ).toBe(false)
+ expect(organizationSearchStatsQuerySchema.parse({ organizationId: 'org' })).toEqual({
+ organizationId: 'org',
+ period: '30d',
+ })
+ })
+})
diff --git a/apps/sim/lib/knowledge/search/stats.ts b/apps/sim/lib/knowledge/search/stats.ts
new file mode 100644
index 00000000000..35dbf4ded42
--- /dev/null
+++ b/apps/sim/lib/knowledge/search/stats.ts
@@ -0,0 +1,74 @@
+export const SEARCH_STATS_PERIODS = ['today', '3d', '7d', '14d', '30d', '90d', 'custom'] as const
+export const SEARCH_STATS_SURFACES = [
+ 'dashboard',
+ 'copilot',
+ 'mcp',
+ 'slack',
+ 'api',
+ 'workflow',
+ 'other',
+] as const
+export const SEARCH_STATS_MAX_DAYS = 90
+const DAY_MS = 86_400_000
+
+export const SEARCH_STATS_PEOPLE_LIMIT = 20
+export const SEARCH_STATS_SOURCE_LIMIT = 100
+
+export const SEARCH_STATS_SURFACE_LABELS: Record<(typeof SEARCH_STATS_SURFACES)[number], string> = {
+ dashboard: 'Search',
+ copilot: 'Assistant',
+ mcp: 'MCP',
+ slack: 'Slack',
+ api: 'API',
+ workflow: 'Workflows',
+ other: 'Other',
+}
+
+export interface SearchStatsDateRange {
+ startDate?: string
+ endDate?: string
+}
+
+/** Checks the date-only UTC range shared by the picker, API, and aggregate query. */
+export function getSearchStatsRangeError(
+ range: SearchStatsDateRange,
+ now = new Date()
+): string | null {
+ const { startDate, endDate } = range
+ if (!startDate || !endDate) return 'Select both a start date and an end date.'
+ for (const date of [startDate, endDate]) {
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) return 'Use dates in YYYY-MM-DD format.'
+ const parsed = new Date(`${date}T00:00:00.000Z`)
+ if (!Number.isFinite(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== date) {
+ return 'Select valid calendar dates.'
+ }
+ }
+ if (endDate < startDate) return 'The end date must be on or after the start date.'
+ if (endDate > now.toISOString().slice(0, 10)) return 'Select dates on or before today (UTC).'
+ const days = (Date.parse(endDate) - Date.parse(startDate)) / DAY_MS + 1
+ if (days > SEARCH_STATS_MAX_DAYS)
+ return `Select a range of ${SEARCH_STATS_MAX_DAYS} days or fewer.`
+ return null
+}
+
+/** Daily UTC buckets, with the selected end date included and today capped at the current time. */
+export function getSearchStatsWindow(
+ period: (typeof SEARCH_STATS_PERIODS)[number],
+ now = new Date(),
+ range: SearchStatsDateRange = {}
+) {
+ if (period === 'custom') {
+ const error = getSearchStatsRangeError(range, now)
+ if (error) throw new Error(error)
+ const start = new Date(`${range.startDate}T00:00:00.000Z`)
+ const endDay = new Date(`${range.endDate}T00:00:00.000Z`)
+ const end = new Date(Math.min(endDay.getTime() + DAY_MS, now.getTime()))
+ const days = (endDay.getTime() - start.getTime()) / DAY_MS + 1
+ return { start, end, days }
+ }
+ const daysByPeriod = { today: 1, '3d': 3, '7d': 7, '14d': 14, '30d': 30, '90d': 90 } as const
+ const days = daysByPeriod[period]
+ const start = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()))
+ start.setUTCDate(start.getUTCDate() - days + 1)
+ return { start, end: now, days }
+}
diff --git a/apps/sim/lib/selectors/application/execute-selector.test.ts b/apps/sim/lib/selectors/application/execute-selector.test.ts
index 60f75064fa7..a0aa015b5b8 100644
--- a/apps/sim/lib/selectors/application/execute-selector.test.ts
+++ b/apps/sim/lib/selectors/application/execute-selector.test.ts
@@ -20,6 +20,15 @@ const mocks = vi.hoisted(() => ({
resolveReferences: vi.fn(),
resolveScope: vi.fn(),
sanitize: vi.fn(),
+ authorizePersonalSearch: vi.fn(),
+ requireOrganizationMembership: vi.fn(),
+}))
+
+vi.mock('@/lib/knowledge/application/personal-search-account', () => ({
+ authorizePersonalSearchSetup: mocks.authorizePersonalSearch,
+}))
+vi.mock('@/lib/core/application/organization-authorization', () => ({
+ requireOrganizationMembership: mocks.requireOrganizationMembership,
}))
vi.mock('@sim/audit', () => ({ recordAudit: vi.fn() }))
@@ -157,6 +166,105 @@ describe('executeSelector', () => {
])
})
+ it('keeps generic organization browsing admin-only', async () => {
+ mocks.requireOrganizationMembership.mockRejectedValueOnce(new Error('Admin required'))
+ await expect(
+ execute({ scope: { kind: 'organization', organizationId: 'org-1' } })
+ ).rejects.toThrow('Admin required')
+ expect(mocks.requireOrganizationMembership).toHaveBeenCalledWith(
+ principal,
+ 'org-1',
+ 'admin',
+ 'knowledge.use'
+ )
+ expect(mocks.authorizePersonalSearch).not.toHaveBeenCalled()
+ expect(mocks.getAttachment).not.toHaveBeenCalled()
+ })
+
+ it('rejects a personal setup marker outside its approved provider selector and organization scope', async () => {
+ await expect(execute({ personalSearchSetup: 'jira' })).rejects.toBeInstanceOf(
+ SelectorContextUnavailableError
+ )
+ await expect(
+ execute({
+ scope: { kind: 'organization', organizationId: 'org-1' },
+ selectorKey: 'jira.issues',
+ personalSearchSetup: 'jira',
+ })
+ ).rejects.toBeInstanceOf(SelectorContextUnavailableError)
+ expect(mocks.authorizeCredential).not.toHaveBeenCalled()
+ expect(mocks.executeAttachment).not.toHaveBeenCalled()
+ })
+
+ it('requires the personal setup authorization before canonical discovery and provider calls', async () => {
+ mocks.authorizePersonalSearch.mockRejectedValueOnce(new Error('Integration unapproved'))
+ await expect(
+ execute({
+ scope: { kind: 'organization', organizationId: 'org-1' },
+ selectorKey: 'jira.projectKeys',
+ context: { oauthCredential: 'managed-1', domain: 'example.atlassian.net' },
+ personalSearchSetup: 'jira',
+ })
+ ).rejects.toThrow('Integration unapproved')
+ expect(mocks.authorizePersonalSearch).toHaveBeenCalledWith(principal, {
+ organizationId: 'org-1',
+ connectorType: 'jira',
+ })
+ expect(mocks.resolveScope).not.toHaveBeenCalled()
+ expect(mocks.executeAttachment).not.toHaveBeenCalled()
+ })
+
+ it('uses the shared selector execution and records the prepared account access once', async () => {
+ const personalScope = { kind: 'organization' as const, organizationId: 'org-1' }
+ mocks.resolveScope.mockResolvedValueOnce({
+ organizationId: 'org-1',
+ workspaceId: undefined,
+ selectorKey: 'jira.projectKeys',
+ selectorManifest: getSelectorManifestEntry('jira.projectKeys'),
+ selectorScope: personalScope,
+ })
+ mocks.resolveReferences.mockResolvedValueOnce({
+ context: { oauthCredential: 'managed-1', domain: 'example.atlassian.net' },
+ request: { kind: 'list' },
+ references: new Map(),
+ })
+ mocks.authorizeCredential.mockResolvedValueOnce({
+ suppliedId: 'managed-1',
+ providerId: 'jira',
+ personalSearchSetup: { principal, organizationId: 'org-1', connectorType: 'jira' },
+ })
+ mocks.getAttachment.mockReturnValueOnce({
+ destination: 'fixed',
+ credential: { kind: 'stored', field: 'oauthCredential', serviceIds: ['jira'] },
+ auditCredentialUse: true,
+ execute: async (args: ExecuteServerSelectorArgs) => {
+ args.recordCredentialUse?.('jira')
+ args.recordCredentialUse?.('jira')
+ return { kind: 'list', items: [{ id: 'PROJECT', label: 'Project' }] }
+ },
+ })
+ await expect(
+ execute({
+ selectorKey: 'jira.projectKeys',
+ scope: personalScope,
+ context: { oauthCredential: 'managed-1', domain: 'example.atlassian.net' },
+ personalSearchSetup: 'jira',
+ })
+ ).resolves.toEqual({ kind: 'list', items: [{ id: 'PROJECT', label: 'Project' }] })
+ expect(mocks.authorizeCredential).toHaveBeenCalledWith(
+ expect.objectContaining({ personalSearchSetup: 'jira', organizationId: 'org-1' })
+ )
+ expect(mocks.requireOrganizationMembership).not.toHaveBeenCalled()
+ expect(mocks.recordCredentialAccess).toHaveBeenCalledTimes(1)
+ expect(mocks.recordCredentialAccess).toHaveBeenCalledWith(
+ expect.objectContaining({
+ actorId: principal.userId,
+ resourceId: 'managed-1',
+ providerId: 'jira',
+ })
+ )
+ })
+
/**
* The picker is a use of the integration, not a neutral list: it reaches the
* provider's API with the caller's credential. The authorization funnel never
diff --git a/apps/sim/lib/selectors/application/execute-selector.ts b/apps/sim/lib/selectors/application/execute-selector.ts
index 1f70f08cc9f..32ca55fb3b5 100644
--- a/apps/sim/lib/selectors/application/execute-selector.ts
+++ b/apps/sim/lib/selectors/application/execute-selector.ts
@@ -8,6 +8,7 @@ import { defineAuthorizedWorkspaceUseCase } from '@/lib/core/application'
import type { OperationUseCase } from '@/lib/core/application/operation'
import { requireOrganizationMembership } from '@/lib/core/application/organization-authorization'
import { OrchestrationError } from '@/lib/core/orchestration/types'
+import { authorizePersonalSearchSetup } from '@/lib/knowledge/application/personal-search-account'
import { type CredentialAuditRequest, recordCredentialAccess } from '@/lib/oauth/token-resolution'
import { selectorOperations } from '@/lib/selectors/application/operations'
import {
@@ -39,6 +40,8 @@ const logger = createLogger('ExecuteSelector')
export interface ExecuteSelectorInput extends ExecuteSelectorRequest {
signal?: AbortSignal
auditRequest?: CredentialAuditRequest
+ /** Set only by the personal Search setup use case; excluded from the public selector contract. */
+ personalSearchSetup?: 'jira' | 'confluence'
}
function validateAuthorizedInput(
@@ -164,6 +167,7 @@ async function executeAuthorizedSelector(args: {
scope: args.input.scope,
workspaceId: args.context.workspaceId,
organizationId,
+ personalSearchSetup: args.input.personalSearchSetup,
policy: attachment.credential,
protectedValues,
references: resolved.references,
@@ -194,19 +198,24 @@ async function executeAuthorizedSelector(args: {
})
const credentialAccess = credential?.access
+ const credentialResourceId = credential?.personalSearchSetup
+ ? credential.suppliedId
+ : credentialAccess?.resolvedCredentialId
let credentialUseRecorded = false
const recordCredentialUse =
- attachment.auditCredentialUse && credentialAccess?.resolvedCredentialId
+ attachment.auditCredentialUse && credentialResourceId
? (providerId: string) => {
if (credentialUseRecorded) return
credentialUseRecorded = true
recordCredentialAccess({
actorId: args.principal.userId,
workspaceId: args.context.workspaceId ?? null,
- resourceId: credentialAccess.resolvedCredentialId!,
+ resourceId: credentialResourceId,
providerId: credential?.providerId ?? providerId,
credentialType:
- credentialAccess.credentialType === 'service_account' ? 'service_account' : 'oauth',
+ credentialAccess?.credentialType === 'service_account'
+ ? 'service_account'
+ : 'oauth',
auditRequest: args.input.auditRequest,
})
}
@@ -337,14 +346,26 @@ export const executeSelector: OperationUseCase<
auditRequest: args.input.auditRequest ?? args.request,
},
}
- if (args.input.scope.kind !== 'organization') return executeWorkspaceSelector.execute(args)
+ if (args.input.scope.kind !== 'organization') {
+ if (args.input.personalSearchSetup) throw new SelectorContextUnavailableError()
+ return executeWorkspaceSelector.execute(args)
+ }
if (args.principal.kind !== 'session') throw new SelectorContextUnavailableError()
- await requireOrganizationMembership(
- args.principal,
- args.input.scope.organizationId,
- 'admin',
- 'knowledge.use'
- )
+ if (args.input.personalSearchSetup) {
+ const selectorKey =
+ args.input.personalSearchSetup === 'jira' ? 'jira.projectKeys' : 'confluence.spaces'
+ if (args.input.selectorKey !== selectorKey) throw new SelectorContextUnavailableError()
+ await authorizePersonalSearchSetup(args.principal, {
+ organizationId: args.input.scope.organizationId,
+ connectorType: args.input.personalSearchSetup,
+ })
+ } else
+ await requireOrganizationMembership(
+ args.principal,
+ args.input.scope.organizationId,
+ 'admin',
+ 'knowledge.use'
+ )
const context = await resolveSelectorApplicationContext({
selectorKey: args.input.selectorKey as ServerSelectorKey,
scope: args.input.scope,
diff --git a/apps/sim/lib/selectors/server/credentials.test.ts b/apps/sim/lib/selectors/server/credentials.test.ts
index 1276041dfa8..f895d933793 100644
--- a/apps/sim/lib/selectors/server/credentials.test.ts
+++ b/apps/sim/lib/selectors/server/credentials.test.ts
@@ -13,6 +13,8 @@ const mocks = vi.hoisted(() => ({
credentialProviderMatchesService: vi.fn(),
getServiceConfig: vi.fn(),
resolveCredentialTokenBundle: vi.fn(),
+ authorizePersonalSearch: vi.fn(),
+ resolveManagedOAuthToken: vi.fn(),
}))
vi.mock('@/lib/auth/credential-access', () => ({
@@ -28,6 +30,13 @@ vi.mock('@/lib/oauth/credential-service', () => ({
resolveCredentialTokenBundle: mocks.resolveCredentialTokenBundle,
}))
+vi.mock('@/lib/knowledge/application/personal-search-account', () => ({
+ authorizePersonalSearchSetupCredential: mocks.authorizePersonalSearch,
+}))
+vi.mock('@/lib/credentials/managed-oauth', () => ({
+ resolveManagedOAuthToken: mocks.resolveManagedOAuthToken,
+}))
+
vi.mock('@/lib/oauth/utils', () => ({
credentialProviderMatchesService: mocks.credentialProviderMatchesService,
getServiceConfigByServiceId: mocks.getServiceConfig,
@@ -117,6 +126,83 @@ describe('authorizeSelectorCredential', () => {
})
})
+ it('browses with only the member’s own prepared account and refreshes it without the admin credential path', async () => {
+ mocks.authorizePersonalSearch.mockResolvedValue({ id: 'managed-1', providerId: 'jira' })
+ mocks.resolveManagedOAuthToken.mockResolvedValue({ accessToken: 'private-token' })
+ const selected = await authorizeSelectorCredential({
+ principal,
+ context: { oauthCredential: 'managed-1' },
+ scope: { kind: 'organization', organizationId: 'org-1' },
+ organizationId: 'org-1',
+ personalSearchSetup: 'jira',
+ policy: { kind: 'stored', field: 'oauthCredential', serviceIds: ['jira'] },
+ protectedValues: createSelectorProtectedValues(),
+ references: new Map(),
+ })
+ const protectedValues = createSelectorProtectedValues()
+ const recordCredentialUse = vi.fn()
+ await expect(
+ resolveSelectorOAuthAccessToken({
+ credential: selected,
+ serviceId: 'jira',
+ scopes: ['read:jira-work'],
+ protectedValues,
+ recordCredentialUse,
+ })
+ ).resolves.toBe('private-token')
+ expect(mocks.authorizePersonalSearch).toHaveBeenCalledTimes(2)
+ expect(mocks.resolveManagedOAuthToken).toHaveBeenCalledWith({
+ organizationId: 'org-1',
+ credentialId: 'managed-1',
+ expectedProviderId: 'jira',
+ requiredScopes: ['read:jira-work'],
+ })
+ expect(protectedValues.contains('private-token')).toBe(true)
+ expect(recordCredentialUse).toHaveBeenCalledWith('jira')
+ expect(mocks.authorizeOrganizationCredentialUse).not.toHaveBeenCalled()
+ expect(mocks.resolveOrganizationCredentialTokenBundle).not.toHaveBeenCalled()
+ })
+
+ it('rejects a revoked prepared credential before refreshing its token', async () => {
+ mocks.authorizePersonalSearch.mockRejectedValue(new Error('Credential revoked'))
+ await expect(
+ resolveSelectorOAuthAccessToken({
+ credential: {
+ suppliedId: 'managed-1',
+ providerId: 'jira',
+ personalSearchSetup: { principal, organizationId: 'org-1', connectorType: 'jira' },
+ },
+ serviceId: 'jira',
+ protectedValues: createSelectorProtectedValues(),
+ })
+ ).rejects.toThrow('Credential revoked')
+ expect(mocks.resolveManagedOAuthToken).not.toHaveBeenCalled()
+ })
+
+ it('refuses using a prepared Jira credential for another provider or impersonation', async () => {
+ const selected = {
+ suppliedId: 'managed-1',
+ providerId: 'jira',
+ personalSearchSetup: { principal, organizationId: 'org-1', connectorType: 'jira' as const },
+ }
+ await expect(
+ resolveSelectorOAuthAccessToken({
+ credential: selected,
+ serviceId: 'confluence',
+ protectedValues: createSelectorProtectedValues(),
+ })
+ ).rejects.toBeInstanceOf(SelectorConnectionUnavailableError)
+ await expect(
+ resolveSelectorOAuthAccessToken({
+ credential: selected,
+ serviceId: 'jira',
+ impersonateEmail: 'other@example.com',
+ protectedValues: createSelectorProtectedValues(),
+ })
+ ).rejects.toBeInstanceOf(SelectorConnectionUnavailableError)
+ expect(mocks.resolveManagedOAuthToken).not.toHaveBeenCalled()
+ })
+
it('rejects an organization connection for the wrong selector provider', async () => {
mocks.authorizeOrganizationCredentialUse.mockResolvedValue({
credential: { id: 'managed-1', providerId: 'jira', type: 'managed_oauth' },
diff --git a/apps/sim/lib/selectors/server/credentials.ts b/apps/sim/lib/selectors/server/credentials.ts
index 83a152cb560..82f1d9a7bcc 100644
--- a/apps/sim/lib/selectors/server/credentials.ts
+++ b/apps/sim/lib/selectors/server/credentials.ts
@@ -9,6 +9,8 @@ import {
authorizeOrganizationCredentialUse,
resolveOrganizationCredentialTokenBundle,
} from '@/lib/credentials/application/organization-credentials'
+import { resolveManagedOAuthToken } from '@/lib/credentials/managed-oauth'
+import { authorizePersonalSearchSetupCredential } from '@/lib/knowledge/application/personal-search-account'
import { resolveCredentialTokenBundle } from '@/lib/oauth/credential-service'
import { credentialProviderMatchesService, getServiceConfigByServiceId } from '@/lib/oauth/utils'
import { SelectorConnectionUnavailableError } from '@/lib/selectors/server/errors'
@@ -104,6 +106,7 @@ export async function authorizeSelectorCredential(input: {
scope: SelectorScope
workspaceId?: string
organizationId?: string
+ personalSearchSetup?: 'jira' | 'confluence'
policy: SelectorCredentialPolicy
protectedValues: SelectorProtectedValues
references: ReadonlyMap
@@ -111,6 +114,32 @@ export async function authorizeSelectorCredential(input: {
const suppliedId = input.context[input.policy.field]
if (!suppliedId) throw new SelectorConnectionUnavailableError()
+ if (input.personalSearchSetup) {
+ if (
+ input.scope.kind !== 'organization' ||
+ input.principal.kind !== 'session' ||
+ input.organizationId !== input.scope.organizationId ||
+ input.workspaceId ||
+ !input.policy.serviceIds.includes(input.personalSearchSetup)
+ )
+ throw new SelectorConnectionUnavailableError()
+ const row = await authorizePersonalSearchSetupCredential(input.principal, {
+ organizationId: input.scope.organizationId,
+ connectorType: input.personalSearchSetup,
+ credentialId: suppliedId,
+ })
+ input.protectedValues.add(suppliedId, 'reference')
+ return {
+ suppliedId,
+ providerId: row.providerId,
+ personalSearchSetup: {
+ principal: input.principal,
+ organizationId: input.scope.organizationId,
+ connectorType: input.personalSearchSetup,
+ },
+ }
+ }
+
if (input.scope.kind === 'organization') {
if (
input.principal.kind !== 'session' ||
@@ -187,6 +216,31 @@ export async function resolveSelectorOAuthAccessToken(input: {
input.credential.signal?.throwIfAborted()
if (input.credential.fixedToken) return input.credential.fixedToken
+ if (input.credential.personalSearchSetup) {
+ const setup = input.credential.personalSearchSetup
+ if (input.impersonateEmail || input.serviceId !== setup.connectorType) {
+ throw new SelectorConnectionUnavailableError()
+ }
+ await authorizePersonalSearchSetupCredential(setup.principal, {
+ ...setup,
+ credentialId: input.credential.suppliedId,
+ })
+ const result = await waitForSelectorCredentialResolution(
+ resolveManagedOAuthToken({
+ organizationId: setup.organizationId,
+ credentialId: input.credential.suppliedId,
+ expectedProviderId: setup.connectorType,
+ requiredScopes: input.scopes ? [...input.scopes] : [],
+ }),
+ input.credential.signal
+ )
+ input.credential.signal?.throwIfAborted()
+ if (!result?.accessToken) throw new SelectorConnectionUnavailableError()
+ input.protectedValues.add(result.accessToken)
+ input.recordCredentialUse?.(setup.connectorType)
+ return result.accessToken
+ }
+
if (input.credential.organization) {
const result = await waitForSelectorCredentialResolution(
resolveOrganizationCredentialTokenBundle({
diff --git a/apps/sim/lib/selectors/server/providers/confluence.test.ts b/apps/sim/lib/selectors/server/providers/confluence.test.ts
index a4437bb0b90..5faa48c5e68 100644
--- a/apps/sim/lib/selectors/server/providers/confluence.test.ts
+++ b/apps/sim/lib/selectors/server/providers/confluence.test.ts
@@ -134,23 +134,70 @@ describe('Confluence server selector adapters', () => {
)
})
- it('hydrates a legacy numeric value in the key selector without rewriting it', async () => {
- mockFetch.mockResolvedValueOnce(
- new Response(JSON.stringify({ id: '12345', key: 'ENG', name: 'Engineering' }), {
- status: 200,
+ it.each(['current', 'archived'] as const)(
+ 'looks up a numeric %s space key as a key rather than a resource ID',
+ async (status) => {
+ mockFetch.mockImplementation((input: URL) => {
+ const matches = new URL(input).searchParams.get('status') === status
+ return new Response(
+ JSON.stringify({
+ results: matches ? [{ id: '99999', key: '12345', name: 'Numeric key' }] : [],
+ }),
+ { status: 200 }
+ )
})
- )
+ await expect(
+ confluenceSelectorAttachments['confluence.spaces'].execute({
+ ...spaceDetailArgs(),
+ request: { kind: 'detail', id: '12345' },
+ })
+ ).resolves.toEqual({
+ kind: 'detail',
+ item: {
+ id: '12345',
+ label: status === 'archived' ? 'Numeric key (12345) — archived' : 'Numeric key (12345)',
+ },
+ })
+ expect(mockFetch).toHaveBeenCalledTimes(2)
+ for (const [input] of mockFetch.mock.calls) {
+ const url = new URL(String(input))
+ expect(url.pathname).toBe('/ex/confluence/cloud-1/wiki/api/v2/spaces')
+ expect(url.searchParams.get('keys')).toBe('12345')
+ }
+ }
+ )
+
+ it('does not accept a numeric resource ID as a matching space key', async () => {
+ mockFetch.mockImplementation(
+ () =>
+ new Response(
+ JSON.stringify({ results: [{ id: '12345', key: 'ENG', name: 'Engineering' }] }),
+ { status: 200 }
+ )
+ )
await expect(
confluenceSelectorAttachments['confluence.spaces'].execute({
...spaceDetailArgs(),
request: { kind: 'detail', id: '12345' },
})
- ).resolves.toEqual({
- kind: 'detail',
- item: { id: '12345', label: 'Engineering (ENG)' },
- })
- expect(String(mockFetch.mock.calls[0]?.[0])).toContain('/wiki/api/v2/spaces/12345')
+ ).resolves.toEqual({ kind: 'detail', item: null })
+ })
+
+ it('preserves key aliases when hydrating the ID selector', async () => {
+ mockFetch.mockImplementation(
+ () =>
+ new Response(
+ JSON.stringify({ results: [{ id: '12345', key: 'ENG', name: 'Engineering' }] }),
+ { status: 200 }
+ )
+ )
+ await expect(
+ confluenceSelectorAttachments['confluence.spacesById'].execute({
+ ...spaceIdDetailArgs(),
+ request: { kind: 'detail', id: 'ENG' },
+ })
+ ).resolves.toEqual({ kind: 'detail', item: { id: 'ENG', label: 'Engineering (ENG)' } })
})
it('projects provider IDs for block space lists while key selectors remain unchanged', async () => {
@@ -307,6 +354,87 @@ describe('Confluence server selector adapters', () => {
).rejects.toMatchObject({ name: 'SelectorConnectionUnavailableError', status: 403 })
})
+ it.each([
+ { failedStatus: 'current', status: 401 },
+ { failedStatus: 'current', status: 403 },
+ { failedStatus: 'current', status: 429 },
+ { failedStatus: 'archived', status: 401 },
+ { failedStatus: 'archived', status: 403 },
+ { failedStatus: 'archived', status: 429 },
+ ])(
+ 'preserves $status from the $failedStatus lookup when the other status has no matching space',
+ async ({ failedStatus, status }) => {
+ mockFetch.mockImplementation((input: URL) =>
+ new URL(input).searchParams.get('status') === failedStatus
+ ? new Response(null, { status })
+ : Response.json({ results: [{ id: '99999', key: 'OTHER', name: 'Other space' }] })
+ )
+
+ await expect(
+ confluenceSelectorAttachments['confluence.spaces'].execute(spaceDetailArgs())
+ ).rejects.toMatchObject({
+ name:
+ status === 429 ? 'SelectorOptionsUnavailableError' : 'SelectorConnectionUnavailableError',
+ status,
+ })
+ expect(mockFetch).toHaveBeenCalledTimes(2)
+ }
+ )
+
+ it.each(['current', 'archived'] as const)(
+ 'returns an exact %s match even when the other status lookup fails',
+ async (matchingStatus) => {
+ mockFetch.mockImplementation((input: URL) =>
+ new URL(input).searchParams.get('status') === matchingStatus
+ ? Response.json({ results: [{ id: '12345', key: 'ENG', name: 'Engineering' }] })
+ : new Response(null, { status: 429 })
+ )
+
+ await expect(
+ confluenceSelectorAttachments['confluence.spaces'].execute(spaceDetailArgs())
+ ).resolves.toEqual({
+ kind: 'detail',
+ item: {
+ id: 'ENG',
+ label:
+ matchingStatus === 'archived' ? 'Engineering (ENG) — archived' : 'Engineering (ENG)',
+ },
+ })
+ }
+ )
+
+ it('reports a space key missing only when both status lookups succeed without a match', async () => {
+ mockFetch
+ .mockResolvedValueOnce(
+ Response.json({ results: [{ id: '99999', key: 'OTHER', name: 'Other space' }] })
+ )
+ .mockResolvedValueOnce(Response.json({ results: [] }))
+
+ await expect(
+ confluenceSelectorAttachments['confluence.spaces'].execute(spaceDetailArgs())
+ ).resolves.toEqual({ kind: 'detail', item: null })
+ expect(mockFetch).toHaveBeenCalledTimes(2)
+ })
+
+ it('sanitizes an arbitrary partial lookup failure rather than reporting the key missing', async () => {
+ const fetchProviderJson = vi
+ .spyOn(providerHttp, 'fetchProviderJson')
+ .mockRejectedValueOnce(new Error('raw provider failure'))
+ .mockResolvedValueOnce({ results: [] })
+
+ try {
+ await expect(
+ confluenceSelectorAttachments['confluence.spaces'].execute(spaceDetailArgs())
+ ).rejects.toMatchObject({
+ name: 'SelectorOptionsUnavailableError',
+ message: 'Options unavailable',
+ status: 502,
+ })
+ } finally {
+ fetchProviderJson.mockRestore()
+ }
+ })
+
it('preserves the first safe provider failure when both space detail requests fail', async () => {
mockFetch
.mockResolvedValueOnce(new Response(null, { status: 401 }))
diff --git a/apps/sim/lib/selectors/server/providers/confluence.ts b/apps/sim/lib/selectors/server/providers/confluence.ts
index d2ae95ada2a..dfaee7bfb51 100644
--- a/apps/sim/lib/selectors/server/providers/confluence.ts
+++ b/apps/sim/lib/selectors/server/providers/confluence.ts
@@ -135,7 +135,7 @@ async function executeSpaces(args: ExecuteServerSelectorArgs, identifier: 'key'
if (args.request.kind === 'detail') {
const requestedId = args.request.id.trim()
if (!requestedId || requestedId.length > 255) throw new SelectorContextUnavailableError()
- if (/^[1-9][0-9]{0,19}$/.test(requestedId)) {
+ if (identifier === 'id' && /^[1-9][0-9]{0,19}$/.test(requestedId)) {
const space = await fetchProviderJson(
`https://api.atlassian.com/ex/confluence/${auth.cloudId}/wiki/api/v2/spaces/${requestedId}`,
{
@@ -162,12 +162,6 @@ async function executeSpaces(args: ExecuteServerSelectorArgs, identifier: 'key'
requestSpaces({ ...auth, params: paramsFor('archived'), signal: args.signal }),
])
args.signal?.throwIfAborted()
- if (current.status === 'rejected' && archived.status === 'rejected') {
- for (const result of [current, archived]) {
- if (isPublicSelectorError(result.reason)) throw result.reason
- }
- throw new SelectorOptionsUnavailableError()
- }
const spaces = [
...(current.status === 'fulfilled'
? (current.value.results ?? []).map((space) => ({ space, status: 'current' as const }))
@@ -177,6 +171,14 @@ async function executeSpaces(args: ExecuteServerSelectorArgs, identifier: 'key'
: []),
]
const match = spaces.find(({ space }) => space.key === key)
+ if (!match && (current.status === 'rejected' || archived.status === 'rejected')) {
+ for (const result of [current, archived]) {
+ if (result.status === 'rejected' && isPublicSelectorError(result.reason)) {
+ throw result.reason
+ }
+ }
+ throw new SelectorOptionsUnavailableError()
+ }
return detailSelectorResult(
match
? {
diff --git a/apps/sim/lib/selectors/server/providers/credential-bundle.test.ts b/apps/sim/lib/selectors/server/providers/credential-bundle.test.ts
index 58a875458b2..88b453da715 100644
--- a/apps/sim/lib/selectors/server/providers/credential-bundle.test.ts
+++ b/apps/sim/lib/selectors/server/providers/credential-bundle.test.ts
@@ -5,6 +5,15 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
const mockResolveCredentialAccessToken = vi.hoisted(() => vi.fn())
const mockResolveOrganizationToken = vi.hoisted(() => vi.fn())
+const mockOwnAccount = vi.hoisted(() => vi.fn())
+const mockResolveManagedToken = vi.hoisted(() => vi.fn())
+
+vi.mock('@/lib/knowledge/application/personal-search-account', () => ({
+ authorizePersonalSearchSetupCredential: mockOwnAccount,
+}))
+vi.mock('@/lib/credentials/managed-oauth', () => ({
+ resolveManagedOAuthToken: mockResolveManagedToken,
+}))
vi.mock('@/lib/credentials/application/organization-credentials', () => ({
resolveOrganizationCredentialTokenBundle: mockResolveOrganizationToken,
@@ -20,6 +29,42 @@ import { resolveSelectorCredentialBundle } from '@/lib/selectors/server/provider
describe('selector credential bundles', () => {
beforeEach(() => vi.clearAllMocks())
+ it('resolves a personal Atlassian grant through the owned managed-account path', async () => {
+ mockOwnAccount.mockResolvedValue({ id: 'managed-1', providerId: 'jira' })
+ mockResolveManagedToken.mockResolvedValue({ accessToken: 'own-managed-token' })
+ const principal = { kind: 'session', userId: 'member-1', sessionId: 'session-1' } as const
+ const protectedValues = createSelectorProtectedValues()
+ await expect(
+ resolveSelectorCredentialBundle({
+ credential: {
+ suppliedId: 'managed-1',
+ providerId: 'jira',
+ personalSearchSetup: { principal, organizationId: 'org-1', connectorType: 'jira' },
+ },
+ providerId: 'jira',
+ scopes: ['read:jira-work'],
+ protectedValues,
+ })
+ ).resolves.toEqual({ accessToken: 'own-managed-token' })
+ expect(mockOwnAccount).toHaveBeenCalledWith(
+ principal,
+ expect.objectContaining({
+ credentialId: 'managed-1',
+ organizationId: 'org-1',
+ connectorType: 'jira',
+ })
+ )
+ expect(mockResolveManagedToken).toHaveBeenCalledWith({
+ organizationId: 'org-1',
+ credentialId: 'managed-1',
+ expectedProviderId: 'jira',
+ requiredScopes: ['read:jira-work'],
+ })
+ expect(mockResolveOrganizationToken).not.toHaveBeenCalled()
+ expect(mockResolveCredentialAccessToken).not.toHaveBeenCalled()
+ expect(protectedValues.contains('own-managed-token')).toBe(true)
+ })
+
it('protects short credential-bound cloud ids as exact identifiers', async () => {
mockResolveCredentialAccessToken.mockResolvedValue({
accessToken: 'server-only-token',
diff --git a/apps/sim/lib/selectors/server/providers/credential-bundle.ts b/apps/sim/lib/selectors/server/providers/credential-bundle.ts
index 97669ff8a8f..b134cf89047 100644
--- a/apps/sim/lib/selectors/server/providers/credential-bundle.ts
+++ b/apps/sim/lib/selectors/server/providers/credential-bundle.ts
@@ -3,7 +3,10 @@ import {
resolveCredentialTokenBundle,
type ServiceAccountTokenResult,
} from '@/lib/oauth/credential-service'
-import { waitForSelectorCredentialResolution } from '@/lib/selectors/server/credentials'
+import {
+ resolveSelectorOAuthAccessToken,
+ waitForSelectorCredentialResolution,
+} from '@/lib/selectors/server/credentials'
import { SelectorConnectionUnavailableError } from '@/lib/selectors/server/errors'
import type {
AuthorizedSelectorCredential,
@@ -26,6 +29,16 @@ export async function resolveSelectorCredentialBundle(input: {
if (!credential) throw new SelectorConnectionUnavailableError()
credential.signal?.throwIfAborted()
+ if (credential.personalSearchSetup) {
+ if (!input.providerId) throw new SelectorConnectionUnavailableError()
+ return {
+ accessToken: await resolveSelectorOAuthAccessToken({
+ ...input,
+ credential,
+ serviceId: input.providerId,
+ }),
+ }
+ }
if (credential.fixedToken) {
if (input.providerId) {
input.recordCredentialUse?.(credential.providerId ?? input.providerId)
diff --git a/apps/sim/lib/selectors/server/providers/jira.test.ts b/apps/sim/lib/selectors/server/providers/jira.test.ts
index d4ea2fcf665..a3807abda7a 100644
--- a/apps/sim/lib/selectors/server/providers/jira.test.ts
+++ b/apps/sim/lib/selectors/server/providers/jira.test.ts
@@ -87,12 +87,14 @@ describe('Jira server selector adapter', () => {
)
it.each([
- ['jira.projects', 'ENG'],
- ['jira.projectKeys', 'ENG'],
- ['jira.projectKeys', '10001'],
+ ['jira.projects', 'ENG', 'ENG'],
+ ['jira.projects', '10001', '10001'],
+ ['jira.projectKeys', 'ENG', 'ENG'],
+ ['jira.projectKeys', '10001', 'ENG'],
+ ['jira.projectKeys', 'LEGACY', 'ENG'],
] as const)(
- 'hydrates %s selection %s without replacing its saved value',
- async (selectorKey, id) => {
+ 'hydrates %s selection %s as %s, preserving ID-selector aliases and returning canonical keys',
+ async (selectorKey, id, expectedId) => {
mockFetch.mockResolvedValueOnce(
new Response(JSON.stringify({ id: '10001', key: 'ENG', name: 'Engineering' }), {
status: 200,
@@ -107,7 +109,7 @@ describe('Jira server selector adapter', () => {
})
).resolves.toEqual({
kind: 'detail',
- item: { id, label: 'Engineering' },
+ item: { id: expectedId, label: 'Engineering' },
})
expect(String(mockFetch.mock.calls[0]?.[0])).toBe(
`https://api.atlassian.com/ex/jira/cloud-1/rest/api/3/project/${id}`
diff --git a/apps/sim/lib/selectors/server/providers/jira.ts b/apps/sim/lib/selectors/server/providers/jira.ts
index bf5e028837a..e27f4017516 100644
--- a/apps/sim/lib/selectors/server/providers/jira.ts
+++ b/apps/sim/lib/selectors/server/providers/jira.ts
@@ -149,7 +149,10 @@ async function getProject(args: ExecuteServerSelectorArgs, projectId: string) {
)
const parsed = jiraProjectSchema.safeParse(body)
if (!parsed.success) throw new SelectorOptionsUnavailableError()
- return { id: projectId, label: parsed.data.name }
+ return {
+ id: args.selectorKey === 'jira.projectKeys' ? parsed.data.key : projectId,
+ label: parsed.data.name,
+ }
}
async function fetchIssues(
diff --git a/apps/sim/lib/selectors/server/types.ts b/apps/sim/lib/selectors/server/types.ts
index 05546532f2d..a8d12395bb6 100644
--- a/apps/sim/lib/selectors/server/types.ts
+++ b/apps/sim/lib/selectors/server/types.ts
@@ -51,6 +51,11 @@ export type SelectorCredentialPolicy =
export interface AuthorizedSelectorCredential {
suppliedId: string
organization?: { principal: SessionPrincipal; organizationId: string }
+ personalSearchSetup?: {
+ principal: SessionPrincipal
+ organizationId: string
+ connectorType: 'jira' | 'confluence'
+ }
access?: CredentialAccessResult
fixedToken?: string
/** Trusted provider id loaded during server-side credential binding. */
diff --git a/apps/sim/lib/selectors/types.ts b/apps/sim/lib/selectors/types.ts
index c97382e32f0..2a20eac3cde 100644
--- a/apps/sim/lib/selectors/types.ts
+++ b/apps/sim/lib/selectors/types.ts
@@ -104,6 +104,13 @@ export type SelectorScope =
workspaceId: string
}
+/** Chooses a dedicated client transport without granting access through the generic selector API. */
+export interface SelectorSurface {
+ kind: 'personal-search-setup'
+ organizationId: string
+ connectorType: 'jira' | 'confluence'
+}
+
export type SelectorRequest =
| {
kind: 'list'
diff --git a/apps/sim/lib/sim-search/connection-labels.ts b/apps/sim/lib/sim-search/connection-labels.ts
new file mode 100644
index 00000000000..da7ee159a52
--- /dev/null
+++ b/apps/sim/lib/sim-search/connection-labels.ts
@@ -0,0 +1,71 @@
+interface SearchConnectionLabels {
+ add: string
+ title: string
+ empty: string
+ searchPlaceholder: string
+}
+
+const CONNECTION_LABELS: Record = {
+ confluence: {
+ add: 'Add Confluence site',
+ title: 'Connect Confluence site',
+ empty: 'No Confluence sites connected.',
+ searchPlaceholder: 'Search Confluence sites...',
+ },
+ jira: {
+ add: 'Add projects',
+ title: 'Add Jira projects',
+ empty: 'No Jira projects added.',
+ searchPlaceholder: 'Search Jira projects...',
+ },
+ github: {
+ add: 'Add repository',
+ title: 'Add GitHub repository',
+ empty: 'No GitHub repositories added.',
+ searchPlaceholder: 'Search GitHub repositories...',
+ },
+ gitlab: {
+ add: 'Add project',
+ title: 'Add GitLab project',
+ empty: 'No GitLab projects added.',
+ searchPlaceholder: 'Search GitLab projects...',
+ },
+ slack: {
+ add: 'Add channels or DMs',
+ title: 'Choose Slack channels and DMs',
+ empty: 'No Slack connections yet.',
+ searchPlaceholder: 'Search Slack connections...',
+ },
+}
+const GOOGLE_NAMES: Record = {
+ gmail: 'Gmail',
+ google_calendar: 'Google Calendar',
+ google_drive: 'Google Drive',
+}
+
+/** Labels describe the connection being configured, not the internal connector record. */
+export function getSearchConnectionLabels(
+ connectorType: string,
+ accessMode: 'admin' | 'members' | 'workspace' = 'admin'
+): SearchConnectionLabels {
+ const googleName = GOOGLE_NAMES[connectorType]
+ if (googleName) {
+ return {
+ add: accessMode === 'members' ? 'Set up member accounts' : 'Connect service account',
+ title:
+ accessMode === 'members'
+ ? `Set up ${googleName} member accounts`
+ : `Connect ${googleName} service account`,
+ empty: `No ${googleName} connections yet.`,
+ searchPlaceholder: `Search ${googleName} connections...`,
+ }
+ }
+ return (
+ CONNECTION_LABELS[connectorType] ?? {
+ add: 'Add connection',
+ title: 'Add connection',
+ empty: 'No connections yet.',
+ searchPlaceholder: 'Search connections...',
+ }
+ )
+}
diff --git a/apps/sim/lib/sim-search/personal-source-setup.ts b/apps/sim/lib/sim-search/personal-source-setup.ts
new file mode 100644
index 00000000000..8afb1049c9f
--- /dev/null
+++ b/apps/sim/lib/sim-search/personal-source-setup.ts
@@ -0,0 +1,2 @@
+/** Personal Atlassian setup stores an explicit, bounded snapshot of selected project or space keys. */
+export const MAX_PERSONAL_SOURCE_SETUP_KEYS = 1000
diff --git a/packages/db/migrations/0338_organization_search_activity.sql b/packages/db/migrations/0338_organization_search_activity.sql
new file mode 100644
index 00000000000..54d5a2ee6f8
--- /dev/null
+++ b/packages/db/migrations/0338_organization_search_activity.sql
@@ -0,0 +1,16 @@
+CREATE TABLE "organization_search_invocation" (
+ "id" text PRIMARY KEY NOT NULL,
+ "organization_id" text NOT NULL,
+ "user_id" text,
+ "surface" text NOT NULL,
+ "source_types" text[] NOT NULL,
+ "result_count" integer NOT NULL,
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
+ CONSTRAINT "organization_search_invocation_result_count_bounds" CHECK ("organization_search_invocation"."result_count" BETWEEN 0 AND 100),
+ CONSTRAINT "organization_search_invocation_source_types_bounds" CHECK (cardinality("organization_search_invocation"."source_types") <= 100)
+);
+--> statement-breakpoint
+ALTER TABLE "organization_search_invocation" ADD CONSTRAINT "organization_search_invocation_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "organization_search_invocation" ADD CONSTRAINT "organization_search_invocation_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
+CREATE INDEX "organization_search_invocation_org_created_idx" ON "organization_search_invocation" USING btree ("organization_id","created_at");--> statement-breakpoint
+CREATE INDEX "organization_search_invocation_user_idx" ON "organization_search_invocation" USING btree ("user_id");
\ No newline at end of file
diff --git a/packages/db/migrations/meta/0338_snapshot.json b/packages/db/migrations/meta/0338_snapshot.json
new file mode 100644
index 00000000000..04095d33534
--- /dev/null
+++ b/packages/db/migrations/meta/0338_snapshot.json
@@ -0,0 +1,26162 @@
+{
+ "id": "980173df-14ec-401b-89a6-53d1999bc12d",
+ "prevId": "52816097-17cd-473b-975a-f7b08169445d",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.academy_certificate": {
+ "name": "academy_certificate",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "course_id": {
+ "name": "course_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "academy_cert_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'active'"
+ },
+ "issued_at": {
+ "name": "issued_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "certificate_number": {
+ "name": "certificate_number",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "academy_certificate_user_id_idx": {
+ "name": "academy_certificate_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "academy_certificate_course_id_idx": {
+ "name": "academy_certificate_course_id_idx",
+ "columns": [
+ {
+ "expression": "course_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "academy_certificate_user_course_unique": {
+ "name": "academy_certificate_user_course_unique",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "course_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "academy_certificate_status_idx": {
+ "name": "academy_certificate_status_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "academy_certificate_user_id_user_id_fk": {
+ "name": "academy_certificate_user_id_user_id_fk",
+ "tableFrom": "academy_certificate",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "academy_certificate_certificate_number_unique": {
+ "name": "academy_certificate_certificate_number_unique",
+ "nullsNotDistinct": false,
+ "columns": ["certificate_number"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.account": {
+ "name": "account",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "account_id": {
+ "name": "account_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "id_token": {
+ "name": "id_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "access_token_expires_at": {
+ "name": "access_token_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token_expires_at": {
+ "name": "refresh_token_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "oauth_config": {
+ "name": "oauth_config",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "account_user_id_idx": {
+ "name": "account_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_account_on_account_id_provider_id": {
+ "name": "idx_account_on_account_id_provider_id",
+ "columns": [
+ {
+ "expression": "account_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "account_user_id_user_id_fk": {
+ "name": "account_user_id_user_id_fk",
+ "tableFrom": "account",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.api_key": {
+ "name": "api_key",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "key_hash": {
+ "name": "key_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'personal'"
+ },
+ "last_used": {
+ "name": "last_used",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "api_key_workspace_type_idx": {
+ "name": "api_key_workspace_type_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "api_key_user_type_idx": {
+ "name": "api_key_user_type_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "api_key_key_hash_idx": {
+ "name": "api_key_key_hash_idx",
+ "columns": [
+ {
+ "expression": "key_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "api_key_user_id_user_id_fk": {
+ "name": "api_key_user_id_user_id_fk",
+ "tableFrom": "api_key",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "api_key_workspace_id_workspace_id_fk": {
+ "name": "api_key_workspace_id_workspace_id_fk",
+ "tableFrom": "api_key",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "api_key_created_by_user_id_fk": {
+ "name": "api_key_created_by_user_id_fk",
+ "tableFrom": "api_key",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "api_key_key_unique": {
+ "name": "api_key_key_unique",
+ "nullsNotDistinct": false,
+ "columns": ["key"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "workspace_type_check": {
+ "name": "workspace_type_check",
+ "value": "(type = 'workspace' AND workspace_id IS NOT NULL) OR (type = 'personal' AND workspace_id IS NULL)"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.async_jobs": {
+ "name": "async_jobs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "payload": {
+ "name": "payload",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "run_at": {
+ "name": "run_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "attempts": {
+ "name": "attempts",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "max_attempts": {
+ "name": "max_attempts",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 3
+ },
+ "error": {
+ "name": "error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "output": {
+ "name": "output",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "async_jobs_status_started_at_idx": {
+ "name": "async_jobs_status_started_at_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "async_jobs_status_completed_at_idx": {
+ "name": "async_jobs_status_completed_at_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "completed_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "async_jobs_schedule_pending_run_at_idx": {
+ "name": "async_jobs_schedule_pending_run_at_idx",
+ "columns": [
+ {
+ "expression": "run_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'pending'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "async_jobs_schedule_processing_started_at_idx": {
+ "name": "async_jobs_schedule_processing_started_at_idx",
+ "columns": [
+ {
+ "expression": "started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'processing'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "async_jobs_schedule_unreconciled_terminal_idx": {
+ "name": "async_jobs_schedule_unreconciled_terminal_idx",
+ "columns": [
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" IN ('completed', 'failed', 'cancelled') AND COALESCE(\"async_jobs\".\"metadata\" ->> 'scheduleReconciled', 'false') <> 'true'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.audit_log": {
+ "name": "audit_log",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "actor_id": {
+ "name": "actor_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "action": {
+ "name": "action",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "resource_type": {
+ "name": "resource_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "resource_id": {
+ "name": "resource_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "actor_name": {
+ "name": "actor_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "actor_email": {
+ "name": "actor_email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "resource_name": {
+ "name": "resource_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'{}'"
+ },
+ "ip_address": {
+ "name": "ip_address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_agent": {
+ "name": "user_agent",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "audit_log_workspace_created_idx": {
+ "name": "audit_log_workspace_created_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "audit_log_workspace_created_at_id_idx": {
+ "name": "audit_log_workspace_created_at_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "date_trunc('milliseconds', \"created_at\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "audit_log_actor_created_idx": {
+ "name": "audit_log_actor_created_idx",
+ "columns": [
+ {
+ "expression": "actor_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "audit_log_resource_idx": {
+ "name": "audit_log_resource_idx",
+ "columns": [
+ {
+ "expression": "resource_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "resource_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "audit_log_action_idx": {
+ "name": "audit_log_action_idx",
+ "columns": [
+ {
+ "expression": "action",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "audit_log_workspace_id_workspace_id_fk": {
+ "name": "audit_log_workspace_id_workspace_id_fk",
+ "tableFrom": "audit_log",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "audit_log_actor_id_user_id_fk": {
+ "name": "audit_log_actor_id_user_id_fk",
+ "tableFrom": "audit_log",
+ "tableTo": "user",
+ "columnsFrom": ["actor_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.background_work_status": {
+ "name": "background_work_status",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "kind": {
+ "name": "kind",
+ "type": "background_work_kind",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "background_work_status_value",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "message": {
+ "name": "message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error": {
+ "name": "error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "background_work_status_workspace_status_idx": {
+ "name": "background_work_status_workspace_status_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "background_work_status_workflow_status_idx": {
+ "name": "background_work_status_workflow_status_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "background_work_status_meta_child_ws_idx": {
+ "name": "background_work_status_meta_child_ws_idx",
+ "columns": [
+ {
+ "expression": "(\"metadata\" ->> 'childWorkspaceId')",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "background_work_status_meta_other_ws_idx": {
+ "name": "background_work_status_meta_other_ws_idx",
+ "columns": [
+ {
+ "expression": "(\"metadata\" ->> 'otherWorkspaceId')",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "background_work_status_workspace_id_workspace_id_fk": {
+ "name": "background_work_status_workspace_id_workspace_id_fk",
+ "tableFrom": "background_work_status",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "background_work_status_workflow_id_workflow_id_fk": {
+ "name": "background_work_status_workflow_id_workflow_id_fk",
+ "tableFrom": "background_work_status",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.chat": {
+ "name": "chat",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "identifier": {
+ "name": "identifier",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_active": {
+ "name": "is_active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "customizations": {
+ "name": "customizations",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'{}'"
+ },
+ "auth_type": {
+ "name": "auth_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'public'"
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "allowed_emails": {
+ "name": "allowed_emails",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'[]'"
+ },
+ "output_configs": {
+ "name": "output_configs",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'[]'"
+ },
+ "include_thinking": {
+ "name": "include_thinking",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "include_tool_calls": {
+ "name": "include_tool_calls",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "archived_at": {
+ "name": "archived_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "identifier_idx": {
+ "name": "identifier_idx",
+ "columns": [
+ {
+ "expression": "identifier",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"chat\".\"archived_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "chat_archived_at_partial_idx": {
+ "name": "chat_archived_at_partial_idx",
+ "columns": [
+ {
+ "expression": "archived_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"chat\".\"archived_at\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_chat_on_workflow_id_archived_at": {
+ "name": "idx_chat_on_workflow_id_archived_at",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "archived_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "chat_workflow_id_workflow_id_fk": {
+ "name": "chat_workflow_id_workflow_id_fk",
+ "tableFrom": "chat",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "chat_user_id_user_id_fk": {
+ "name": "chat_user_id_user_id_fk",
+ "tableFrom": "chat",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.copilot_async_tool_calls": {
+ "name": "copilot_async_tool_calls",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "run_id": {
+ "name": "run_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "checkpoint_id": {
+ "name": "checkpoint_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tool_call_id": {
+ "name": "tool_call_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tool_name": {
+ "name": "tool_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "args": {
+ "name": "args",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "status": {
+ "name": "status",
+ "type": "copilot_async_tool_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "result": {
+ "name": "result",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error": {
+ "name": "error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "permission_decision": {
+ "name": "permission_decision",
+ "type": "copilot_tool_permission_decision",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "permission_decided_at": {
+ "name": "permission_decided_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "claimed_at": {
+ "name": "claimed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "claimed_by": {
+ "name": "claimed_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "copilot_async_tool_calls_run_id_idx": {
+ "name": "copilot_async_tool_calls_run_id_idx",
+ "columns": [
+ {
+ "expression": "run_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_async_tool_calls_checkpoint_id_idx": {
+ "name": "copilot_async_tool_calls_checkpoint_id_idx",
+ "columns": [
+ {
+ "expression": "checkpoint_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_async_tool_calls_status_idx": {
+ "name": "copilot_async_tool_calls_status_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_async_tool_calls_run_status_idx": {
+ "name": "copilot_async_tool_calls_run_status_idx",
+ "columns": [
+ {
+ "expression": "run_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_async_tool_calls_tool_call_id_unique": {
+ "name": "copilot_async_tool_calls_tool_call_id_unique",
+ "columns": [
+ {
+ "expression": "tool_call_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "copilot_async_tool_calls_run_id_copilot_runs_id_fk": {
+ "name": "copilot_async_tool_calls_run_id_copilot_runs_id_fk",
+ "tableFrom": "copilot_async_tool_calls",
+ "tableTo": "copilot_runs",
+ "columnsFrom": ["run_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk": {
+ "name": "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk",
+ "tableFrom": "copilot_async_tool_calls",
+ "tableTo": "copilot_run_checkpoints",
+ "columnsFrom": ["checkpoint_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.copilot_chats": {
+ "name": "copilot_chats",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "type": {
+ "name": "type",
+ "type": "chat_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'copilot'"
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "model": {
+ "name": "model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'claude-3-7-sonnet-latest'"
+ },
+ "conversation_id": {
+ "name": "conversation_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "external_conversation_key": {
+ "name": "external_conversation_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "external_conversation_metadata": {
+ "name": "external_conversation_metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "preview_yaml": {
+ "name": "preview_yaml",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "plan_artifact": {
+ "name": "plan_artifact",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "config": {
+ "name": "config",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "resources": {
+ "name": "resources",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'"
+ },
+ "auto_allowed_tools": {
+ "name": "auto_allowed_tools",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'"
+ },
+ "last_seen_at": {
+ "name": "last_seen_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "pinned": {
+ "name": "pinned",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "copilot_chats_organization_id_idx": {
+ "name": "copilot_chats_organization_id_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_chats_external_conversation_unique": {
+ "name": "copilot_chats_external_conversation_unique",
+ "columns": [
+ {
+ "expression": "external_conversation_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"copilot_chats\".\"external_conversation_key\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_chats_user_org_created_idx": {
+ "name": "copilot_chats_user_org_created_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_chats_user_id_idx": {
+ "name": "copilot_chats_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_chats_workflow_id_idx": {
+ "name": "copilot_chats_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_chats_user_workflow_idx": {
+ "name": "copilot_chats_user_workflow_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_chats_user_workspace_idx": {
+ "name": "copilot_chats_user_workspace_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_chats_created_at_idx": {
+ "name": "copilot_chats_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_chats_updated_at_idx": {
+ "name": "copilot_chats_updated_at_idx",
+ "columns": [
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_chats_workspace_created_at_id_idx": {
+ "name": "copilot_chats_workspace_created_at_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "date_trunc('milliseconds', \"created_at\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_chats_user_workspace_deleted_partial_idx": {
+ "name": "copilot_chats_user_workspace_deleted_partial_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"copilot_chats\".\"deleted_at\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "copilot_chats_user_id_user_id_fk": {
+ "name": "copilot_chats_user_id_user_id_fk",
+ "tableFrom": "copilot_chats",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "copilot_chats_workflow_id_workflow_id_fk": {
+ "name": "copilot_chats_workflow_id_workflow_id_fk",
+ "tableFrom": "copilot_chats",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "copilot_chats_workspace_id_workspace_id_fk": {
+ "name": "copilot_chats_workspace_id_workspace_id_fk",
+ "tableFrom": "copilot_chats",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "copilot_chats_organization_id_organization_id_fk": {
+ "name": "copilot_chats_organization_id_organization_id_fk",
+ "tableFrom": "copilot_chats",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "copilot_chats_owner_check": {
+ "name": "copilot_chats_owner_check",
+ "value": "num_nonnulls(\"copilot_chats\".\"workspace_id\", \"copilot_chats\".\"organization_id\") <= 1"
+ },
+ "copilot_chats_organization_workflow_check": {
+ "name": "copilot_chats_organization_workflow_check",
+ "value": "\"copilot_chats\".\"organization_id\" IS NULL OR \"copilot_chats\".\"workflow_id\" IS NULL"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.copilot_feedback": {
+ "name": "copilot_feedback",
+ "schema": "",
+ "columns": {
+ "feedback_id": {
+ "name": "feedback_id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "chat_id": {
+ "name": "chat_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_query": {
+ "name": "user_query",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "agent_response": {
+ "name": "agent_response",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_positive": {
+ "name": "is_positive",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "feedback": {
+ "name": "feedback",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "workflow_yaml": {
+ "name": "workflow_yaml",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "copilot_feedback_user_id_idx": {
+ "name": "copilot_feedback_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_feedback_chat_id_idx": {
+ "name": "copilot_feedback_chat_id_idx",
+ "columns": [
+ {
+ "expression": "chat_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_feedback_user_chat_idx": {
+ "name": "copilot_feedback_user_chat_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "chat_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_feedback_is_positive_idx": {
+ "name": "copilot_feedback_is_positive_idx",
+ "columns": [
+ {
+ "expression": "is_positive",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_feedback_created_at_idx": {
+ "name": "copilot_feedback_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "copilot_feedback_user_id_user_id_fk": {
+ "name": "copilot_feedback_user_id_user_id_fk",
+ "tableFrom": "copilot_feedback",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "copilot_feedback_chat_id_copilot_chats_id_fk": {
+ "name": "copilot_feedback_chat_id_copilot_chats_id_fk",
+ "tableFrom": "copilot_feedback",
+ "tableTo": "copilot_chats",
+ "columnsFrom": ["chat_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.copilot_messages": {
+ "name": "copilot_messages",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "chat_id": {
+ "name": "chat_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "message_id": {
+ "name": "message_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content": {
+ "name": "content",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "stream_id": {
+ "name": "stream_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "parent_message_id": {
+ "name": "parent_message_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "model": {
+ "name": "model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tokens_in": {
+ "name": "tokens_in",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tokens_out": {
+ "name": "tokens_out",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "seq": {
+ "name": "seq",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "copilot_messages_chat_message_unique": {
+ "name": "copilot_messages_chat_message_unique",
+ "columns": [
+ {
+ "expression": "chat_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "message_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_messages_chat_created_at_idx": {
+ "name": "copilot_messages_chat_created_at_idx",
+ "columns": [
+ {
+ "expression": "chat_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"copilot_messages\".\"deleted_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_messages_chat_seq_idx": {
+ "name": "copilot_messages_chat_seq_idx",
+ "columns": [
+ {
+ "expression": "chat_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "seq",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"copilot_messages\".\"deleted_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_messages_chat_stream_idx": {
+ "name": "copilot_messages_chat_stream_idx",
+ "columns": [
+ {
+ "expression": "chat_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "stream_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"copilot_messages\".\"stream_id\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_messages_user_created_at_idx": {
+ "name": "copilot_messages_user_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "chat_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "message_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"copilot_messages\".\"role\" = 'user' AND \"copilot_messages\".\"deleted_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "copilot_messages_chat_id_copilot_chats_id_fk": {
+ "name": "copilot_messages_chat_id_copilot_chats_id_fk",
+ "tableFrom": "copilot_messages",
+ "tableTo": "copilot_chats",
+ "columnsFrom": ["chat_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.copilot_run_checkpoints": {
+ "name": "copilot_run_checkpoints",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "run_id": {
+ "name": "run_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "pending_tool_call_id": {
+ "name": "pending_tool_call_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "conversation_snapshot": {
+ "name": "conversation_snapshot",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "agent_state": {
+ "name": "agent_state",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "provider_request": {
+ "name": "provider_request",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "copilot_run_checkpoints_run_id_idx": {
+ "name": "copilot_run_checkpoints_run_id_idx",
+ "columns": [
+ {
+ "expression": "run_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_run_checkpoints_pending_tool_call_id_idx": {
+ "name": "copilot_run_checkpoints_pending_tool_call_id_idx",
+ "columns": [
+ {
+ "expression": "pending_tool_call_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_run_checkpoints_run_pending_tool_unique": {
+ "name": "copilot_run_checkpoints_run_pending_tool_unique",
+ "columns": [
+ {
+ "expression": "run_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "pending_tool_call_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "copilot_run_checkpoints_run_id_copilot_runs_id_fk": {
+ "name": "copilot_run_checkpoints_run_id_copilot_runs_id_fk",
+ "tableFrom": "copilot_run_checkpoints",
+ "tableTo": "copilot_runs",
+ "columnsFrom": ["run_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.copilot_runs": {
+ "name": "copilot_runs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "execution_id": {
+ "name": "execution_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "parent_run_id": {
+ "name": "parent_run_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "chat_id": {
+ "name": "chat_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "stream_id": {
+ "name": "stream_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "agent": {
+ "name": "agent",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "model": {
+ "name": "model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "copilot_run_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'active'"
+ },
+ "request_context": {
+ "name": "request_context",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "error": {
+ "name": "error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "copilot_runs_execution_id_idx": {
+ "name": "copilot_runs_execution_id_idx",
+ "columns": [
+ {
+ "expression": "execution_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_runs_parent_run_id_idx": {
+ "name": "copilot_runs_parent_run_id_idx",
+ "columns": [
+ {
+ "expression": "parent_run_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_runs_chat_id_idx": {
+ "name": "copilot_runs_chat_id_idx",
+ "columns": [
+ {
+ "expression": "chat_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_runs_user_id_idx": {
+ "name": "copilot_runs_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_runs_workflow_id_idx": {
+ "name": "copilot_runs_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_runs_workspace_id_idx": {
+ "name": "copilot_runs_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_runs_status_idx": {
+ "name": "copilot_runs_status_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_runs_chat_execution_idx": {
+ "name": "copilot_runs_chat_execution_idx",
+ "columns": [
+ {
+ "expression": "chat_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "execution_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_runs_execution_started_at_idx": {
+ "name": "copilot_runs_execution_started_at_idx",
+ "columns": [
+ {
+ "expression": "execution_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_runs_workspace_completed_at_id_idx": {
+ "name": "copilot_runs_workspace_completed_at_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "date_trunc('milliseconds', \"completed_at\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_runs_stream_id_unique": {
+ "name": "copilot_runs_stream_id_unique",
+ "columns": [
+ {
+ "expression": "stream_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "copilot_runs_chat_id_copilot_chats_id_fk": {
+ "name": "copilot_runs_chat_id_copilot_chats_id_fk",
+ "tableFrom": "copilot_runs",
+ "tableTo": "copilot_chats",
+ "columnsFrom": ["chat_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "copilot_runs_user_id_user_id_fk": {
+ "name": "copilot_runs_user_id_user_id_fk",
+ "tableFrom": "copilot_runs",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "copilot_runs_workflow_id_workflow_id_fk": {
+ "name": "copilot_runs_workflow_id_workflow_id_fk",
+ "tableFrom": "copilot_runs",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "copilot_runs_workspace_id_workspace_id_fk": {
+ "name": "copilot_runs_workspace_id_workspace_id_fk",
+ "tableFrom": "copilot_runs",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.copilot_workflow_read_hashes": {
+ "name": "copilot_workflow_read_hashes",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "chat_id": {
+ "name": "chat_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "hash": {
+ "name": "hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "copilot_workflow_read_hashes_chat_id_idx": {
+ "name": "copilot_workflow_read_hashes_chat_id_idx",
+ "columns": [
+ {
+ "expression": "chat_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_workflow_read_hashes_workflow_id_idx": {
+ "name": "copilot_workflow_read_hashes_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_workflow_read_hashes_chat_workflow_unique": {
+ "name": "copilot_workflow_read_hashes_chat_workflow_unique",
+ "columns": [
+ {
+ "expression": "chat_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk": {
+ "name": "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk",
+ "tableFrom": "copilot_workflow_read_hashes",
+ "tableTo": "copilot_chats",
+ "columnsFrom": ["chat_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "copilot_workflow_read_hashes_workflow_id_workflow_id_fk": {
+ "name": "copilot_workflow_read_hashes_workflow_id_workflow_id_fk",
+ "tableFrom": "copilot_workflow_read_hashes",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.credential": {
+ "name": "credential",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "slack_app_id": {
+ "name": "slack_app_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "type": {
+ "name": "type",
+ "type": "credential_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "display_name": {
+ "name": "display_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "unredacted": {
+ "name": "unredacted",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "account_id": {
+ "name": "account_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "env_key": {
+ "name": "env_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "env_owner_user_id": {
+ "name": "env_owner_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "encrypted_service_account_key": {
+ "name": "encrypted_service_account_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "encrypted_personal_token": {
+ "name": "encrypted_personal_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "authorization_app_id": {
+ "name": "authorization_app_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "credential_group_enrollment_id": {
+ "name": "credential_group_enrollment_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "credential_group_option_id": {
+ "name": "credential_group_option_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "mcp_server_id": {
+ "name": "mcp_server_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "mcp_oauth_config_version": {
+ "name": "mcp_oauth_config_version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "managed_oauth_scope_version": {
+ "name": "managed_oauth_scope_version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "provider_subject_id": {
+ "name": "provider_subject_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "provider_tenant_id": {
+ "name": "provider_tenant_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "managed_oauth_status": {
+ "name": "managed_oauth_status",
+ "type": "managed_oauth_credential_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "granted_scopes": {
+ "name": "granted_scopes",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "provider_metadata": {
+ "name": "provider_metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "encrypted_oauth_token_set": {
+ "name": "encrypted_oauth_token_set",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "mcp_tools": {
+ "name": "mcp_tools",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "mcp_tools_refreshed_at": {
+ "name": "mcp_tools_refreshed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "granted_at": {
+ "name": "granted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "revoked_at": {
+ "name": "revoked_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "access_token_expires_at": {
+ "name": "access_token_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token_expires_at": {
+ "name": "refresh_token_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_refreshed_at": {
+ "name": "last_refreshed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "credential_organization_id_idx": {
+ "name": "credential_organization_id_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "credential_organization_account_unique": {
+ "name": "credential_organization_account_unique",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "account_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"credential\".\"account_id\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "credential_org_personal_token_unique": {
+ "name": "credential_org_personal_token_unique",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_by",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider_tenant_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider_subject_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"credential\".\"type\" = 'personal_token'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "credential_workspace_id_idx": {
+ "name": "credential_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "credential_type_idx": {
+ "name": "credential_type_idx",
+ "columns": [
+ {
+ "expression": "type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "credential_provider_id_idx": {
+ "name": "credential_provider_id_idx",
+ "columns": [
+ {
+ "expression": "provider_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "credential_account_id_idx": {
+ "name": "credential_account_id_idx",
+ "columns": [
+ {
+ "expression": "account_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "credential_env_owner_user_id_idx": {
+ "name": "credential_env_owner_user_id_idx",
+ "columns": [
+ {
+ "expression": "env_owner_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "credential_group_enrollment_idx": {
+ "name": "credential_group_enrollment_idx",
+ "columns": [
+ {
+ "expression": "credential_group_enrollment_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "credential_mcp_server_idx": {
+ "name": "credential_mcp_server_idx",
+ "columns": [
+ {
+ "expression": "mcp_server_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "credential_group_option_unique": {
+ "name": "credential_group_option_unique",
+ "columns": [
+ {
+ "expression": "credential_group_enrollment_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "credential_group_option_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"credential\".\"type\" = 'managed_oauth'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "credential_managed_mcp_enrollment_server_unique": {
+ "name": "credential_managed_mcp_enrollment_server_unique",
+ "columns": [
+ {
+ "expression": "credential_group_enrollment_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "mcp_server_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"credential\".\"type\" = 'managed_mcp'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "credential_workspace_account_unique": {
+ "name": "credential_workspace_account_unique",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "account_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "account_id IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "credential_workspace_env_unique": {
+ "name": "credential_workspace_env_unique",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "env_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "type = 'env_workspace'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "credential_workspace_personal_env_unique": {
+ "name": "credential_workspace_personal_env_unique",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "env_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "env_owner_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "type = 'env_personal'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "credential_personal_token_identity_unique": {
+ "name": "credential_personal_token_identity_unique",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_by",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider_tenant_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider_subject_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "type = 'personal_token'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "credential_workspace_id_workspace_id_fk": {
+ "name": "credential_workspace_id_workspace_id_fk",
+ "tableFrom": "credential",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "credential_organization_id_organization_id_fk": {
+ "name": "credential_organization_id_organization_id_fk",
+ "tableFrom": "credential",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "credential_slack_app_id_slack_app_id_fk": {
+ "name": "credential_slack_app_id_slack_app_id_fk",
+ "tableFrom": "credential",
+ "tableTo": "slack_app",
+ "columnsFrom": ["slack_app_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "credential_account_id_account_id_fk": {
+ "name": "credential_account_id_account_id_fk",
+ "tableFrom": "credential",
+ "tableTo": "account",
+ "columnsFrom": ["account_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "credential_env_owner_user_id_user_id_fk": {
+ "name": "credential_env_owner_user_id_user_id_fk",
+ "tableFrom": "credential",
+ "tableTo": "user",
+ "columnsFrom": ["env_owner_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "credential_credential_group_enrollment_id_credential_group_enrollment_id_fk": {
+ "name": "credential_credential_group_enrollment_id_credential_group_enrollment_id_fk",
+ "tableFrom": "credential",
+ "tableTo": "credential_group_enrollment",
+ "columnsFrom": ["credential_group_enrollment_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "credential_mcp_server_id_mcp_servers_id_fk": {
+ "name": "credential_mcp_server_id_mcp_servers_id_fk",
+ "tableFrom": "credential",
+ "tableTo": "mcp_servers",
+ "columnsFrom": ["mcp_server_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "credential_created_by_user_id_fk": {
+ "name": "credential_created_by_user_id_fk",
+ "tableFrom": "credential",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "credential_owner_check": {
+ "name": "credential_owner_check",
+ "value": "num_nonnulls(\"credential\".\"workspace_id\", \"credential\".\"organization_id\") = 1"
+ },
+ "credential_organization_type_check": {
+ "name": "credential_organization_type_check",
+ "value": "\"credential\".\"organization_id\" IS NULL OR \"credential\".\"type\" IN ('oauth', 'managed_oauth', 'managed_mcp', 'service_account', 'personal_token')"
+ },
+ "credential_personal_token_source_check": {
+ "name": "credential_personal_token_source_check",
+ "value": "(type::text <> 'personal_token') OR (\n created_by IS NOT NULL\n AND provider_id IS NOT NULL\n AND provider_id = 'gitlab'\n AND provider_subject_id IS NOT NULL\n AND provider_tenant_id IS NOT NULL\n AND encrypted_personal_token IS NOT NULL\n AND granted_scopes IS NOT NULL\n AND cardinality(granted_scopes) > 0\n AND account_id IS NULL\n AND env_key IS NULL\n AND env_owner_user_id IS NULL\n AND authorization_app_id IS NULL\n AND encrypted_oauth_token_set IS NULL\n AND encrypted_service_account_key IS NULL\n AND unredacted = false\n )"
+ },
+ "credential_oauth_source_check": {
+ "name": "credential_oauth_source_check",
+ "value": "(type <> 'oauth') OR (account_id IS NOT NULL AND provider_id IS NOT NULL)"
+ },
+ "credential_managed_oauth_source_check": {
+ "name": "credential_managed_oauth_source_check",
+ "value": "(type::text <> 'managed_oauth') OR (\n account_id IS NULL\n AND provider_id IS NOT NULL\n AND authorization_app_id IS NOT NULL\n AND provider_subject_id IS NOT NULL\n AND managed_oauth_status IS NOT NULL\n AND granted_scopes IS NOT NULL\n AND encrypted_oauth_token_set IS NOT NULL\n AND granted_at IS NOT NULL\n )"
+ },
+ "credential_managed_oauth_group_binding_check": {
+ "name": "credential_managed_oauth_group_binding_check",
+ "value": "(type::text <> 'managed_oauth') OR (\n credential_group_enrollment_id IS NOT NULL\n AND credential_group_option_id IS NOT NULL\n AND managed_oauth_scope_version IS NOT NULL\n AND managed_oauth_scope_version > 0\n )"
+ },
+ "credential_managed_mcp_source_check": {
+ "name": "credential_managed_mcp_source_check",
+ "value": "(type::text <> 'managed_mcp') OR (\n id LIKE 'mcp-cg-%'\n AND account_id IS NULL\n AND provider_id IS NULL\n AND authorization_app_id IS NULL\n AND credential_group_enrollment_id IS NOT NULL\n AND credential_group_option_id IS NULL\n AND mcp_server_id IS NOT NULL\n AND managed_oauth_status IS NOT NULL\n AND (managed_oauth_status <> 'active' OR (\n encrypted_oauth_token_set IS NOT NULL\n AND mcp_tools IS NOT NULL\n ))\n AND granted_at IS NOT NULL\n AND managed_oauth_scope_version IS NULL\n AND provider_subject_id IS NULL\n AND provider_tenant_id IS NULL\n AND granted_scopes IS NULL\n AND provider_metadata IS NULL\n AND created_by IS NULL\n AND env_key IS NULL\n AND env_owner_user_id IS NULL\n AND encrypted_service_account_key IS NULL\n AND unredacted = false\n )"
+ },
+ "credential_creator_source_check": {
+ "name": "credential_creator_source_check",
+ "value": "(type::text = 'managed_mcp') OR created_by IS NOT NULL"
+ },
+ "credential_workspace_env_source_check": {
+ "name": "credential_workspace_env_source_check",
+ "value": "(type <> 'env_workspace') OR (env_key IS NOT NULL AND env_owner_user_id IS NULL)"
+ },
+ "credential_personal_env_source_check": {
+ "name": "credential_personal_env_source_check",
+ "value": "(type <> 'env_personal') OR (env_key IS NOT NULL AND env_owner_user_id IS NOT NULL)"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.credential_group": {
+ "name": "credential_group",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "public_id": {
+ "name": "public_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "options": {
+ "name": "options",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "encrypted_provider_configuration": {
+ "name": "encrypted_provider_configuration",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "credential_group_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'active'"
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "credential_group_organization_id_idx": {
+ "name": "credential_group_organization_id_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "credential_group_organization_unique": {
+ "name": "credential_group_organization_unique",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "credential_group_public_id_unique": {
+ "name": "credential_group_public_id_unique",
+ "columns": [
+ {
+ "expression": "public_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "credential_group_workspace_unique": {
+ "name": "credential_group_workspace_unique",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "credential_group_workspace_id_workspace_id_fk": {
+ "name": "credential_group_workspace_id_workspace_id_fk",
+ "tableFrom": "credential_group",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "credential_group_organization_id_organization_id_fk": {
+ "name": "credential_group_organization_id_organization_id_fk",
+ "tableFrom": "credential_group",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "credential_group_created_by_user_id_fk": {
+ "name": "credential_group_created_by_user_id_fk",
+ "tableFrom": "credential_group",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "credential_group_owner_check": {
+ "name": "credential_group_owner_check",
+ "value": "num_nonnulls(\"credential_group\".\"workspace_id\", \"credential_group\".\"organization_id\") = 1"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.credential_group_enrollment": {
+ "name": "credential_group_enrollment",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "credential_group_id": {
+ "name": "credential_group_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "credential_group_enrollment_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'invited'"
+ },
+ "invitation_token_hash": {
+ "name": "invitation_token_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "invitation_expires_at": {
+ "name": "invitation_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "invited_at": {
+ "name": "invited_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "sent_at": {
+ "name": "sent_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "revoked_at": {
+ "name": "revoked_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_delivery_error": {
+ "name": "last_delivery_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "credential_group_enrollment_group_user_unique": {
+ "name": "credential_group_enrollment_group_user_unique",
+ "columns": [
+ {
+ "expression": "credential_group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"credential_group_enrollment\".\"user_id\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "credential_group_enrollment_user_id_idx": {
+ "name": "credential_group_enrollment_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "credential_group_enrollment_group_email_unique": {
+ "name": "credential_group_enrollment_group_email_unique",
+ "columns": [
+ {
+ "expression": "credential_group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "email",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "credential_group_enrollment_invitation_token_hash_unique": {
+ "name": "credential_group_enrollment_invitation_token_hash_unique",
+ "columns": [
+ {
+ "expression": "invitation_token_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "credential_group_enrollment_group_status_idx": {
+ "name": "credential_group_enrollment_group_status_idx",
+ "columns": [
+ {
+ "expression": "credential_group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "credential_group_enrollment_group_invited_at_id_idx": {
+ "name": "credential_group_enrollment_group_invited_at_id_idx",
+ "columns": [
+ {
+ "expression": "credential_group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "invited_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "credential_group_enrollment_credential_group_id_credential_group_id_fk": {
+ "name": "credential_group_enrollment_credential_group_id_credential_group_id_fk",
+ "tableFrom": "credential_group_enrollment",
+ "tableTo": "credential_group",
+ "columnsFrom": ["credential_group_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "credential_group_enrollment_user_id_user_id_fk": {
+ "name": "credential_group_enrollment_user_id_user_id_fk",
+ "tableFrom": "credential_group_enrollment",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "credential_group_enrollment_created_by_user_id_fk": {
+ "name": "credential_group_enrollment_created_by_user_id_fk",
+ "tableFrom": "credential_group_enrollment",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "credential_group_enrollment_normalized_email_check": {
+ "name": "credential_group_enrollment_normalized_email_check",
+ "value": "\"credential_group_enrollment\".\"email\" = lower(btrim(\"credential_group_enrollment\".\"email\")) AND length(\"credential_group_enrollment\".\"email\") BETWEEN 3 AND 320"
+ },
+ "credential_group_enrollment_invitation_token_hash_length_check": {
+ "name": "credential_group_enrollment_invitation_token_hash_length_check",
+ "value": "length(\"credential_group_enrollment\".\"invitation_token_hash\") = 64"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.credential_member": {
+ "name": "credential_member",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "credential_id": {
+ "name": "credential_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "credential_member_role",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'member'"
+ },
+ "status": {
+ "name": "status",
+ "type": "credential_member_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'active'"
+ },
+ "joined_at": {
+ "name": "joined_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "invited_by": {
+ "name": "invited_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "credential_member_user_id_idx": {
+ "name": "credential_member_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "credential_member_role_idx": {
+ "name": "credential_member_role_idx",
+ "columns": [
+ {
+ "expression": "role",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "credential_member_status_idx": {
+ "name": "credential_member_status_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "credential_member_unique": {
+ "name": "credential_member_unique",
+ "columns": [
+ {
+ "expression": "credential_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "credential_member_credential_id_credential_id_fk": {
+ "name": "credential_member_credential_id_credential_id_fk",
+ "tableFrom": "credential_member",
+ "tableTo": "credential",
+ "columnsFrom": ["credential_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "credential_member_user_id_user_id_fk": {
+ "name": "credential_member_user_id_user_id_fk",
+ "tableFrom": "credential_member",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "credential_member_invited_by_user_id_fk": {
+ "name": "credential_member_invited_by_user_id_fk",
+ "tableFrom": "credential_member",
+ "tableTo": "user",
+ "columnsFrom": ["invited_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.custom_block": {
+ "name": "custom_block",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "icon_url": {
+ "name": "icon_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "inputs": {
+ "name": "inputs",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "outputs": {
+ "name": "outputs",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "trace_child_runs": {
+ "name": "trace_child_runs",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "custom_block_organization_id_idx": {
+ "name": "custom_block_organization_id_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "custom_block_workflow_id_idx": {
+ "name": "custom_block_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "custom_block_organization_type_unique": {
+ "name": "custom_block_organization_type_unique",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "custom_block_organization_id_organization_id_fk": {
+ "name": "custom_block_organization_id_organization_id_fk",
+ "tableFrom": "custom_block",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "custom_block_workflow_id_workflow_id_fk": {
+ "name": "custom_block_workflow_id_workflow_id_fk",
+ "tableFrom": "custom_block",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "custom_block_created_by_user_id_fk": {
+ "name": "custom_block_created_by_user_id_fk",
+ "tableFrom": "custom_block",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.custom_tools": {
+ "name": "custom_tools",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "schema": {
+ "name": "schema",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "code": {
+ "name": "code",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "custom_tools_workspace_id_idx": {
+ "name": "custom_tools_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "custom_tools_workspace_title_unique": {
+ "name": "custom_tools_workspace_title_unique",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "title",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "custom_tools_workspace_id_workspace_id_fk": {
+ "name": "custom_tools_workspace_id_workspace_id_fk",
+ "tableFrom": "custom_tools",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "custom_tools_user_id_user_id_fk": {
+ "name": "custom_tools_user_id_user_id_fk",
+ "tableFrom": "custom_tools",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.data_drain_runs": {
+ "name": "data_drain_runs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "drain_id": {
+ "name": "drain_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "data_drain_run_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "trigger": {
+ "name": "trigger",
+ "type": "data_drain_run_trigger",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "finished_at": {
+ "name": "finished_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rows_exported": {
+ "name": "rows_exported",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "bytes_written": {
+ "name": "bytes_written",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "cursor_before": {
+ "name": "cursor_before",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cursor_after": {
+ "name": "cursor_after",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error": {
+ "name": "error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "locators": {
+ "name": "locators",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ }
+ },
+ "indexes": {
+ "data_drain_runs_drain_started_idx": {
+ "name": "data_drain_runs_drain_started_idx",
+ "columns": [
+ {
+ "expression": "drain_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "data_drain_runs_drain_id_data_drains_id_fk": {
+ "name": "data_drain_runs_drain_id_data_drains_id_fk",
+ "tableFrom": "data_drain_runs",
+ "tableTo": "data_drains",
+ "columnsFrom": ["drain_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.data_drains": {
+ "name": "data_drains",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source": {
+ "name": "source",
+ "type": "data_drain_source",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "destination_type": {
+ "name": "destination_type",
+ "type": "data_drain_destination",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "destination_config": {
+ "name": "destination_config",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "destination_credentials": {
+ "name": "destination_credentials",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "schedule_cadence": {
+ "name": "schedule_cadence",
+ "type": "data_drain_cadence",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "cursor": {
+ "name": "cursor",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_run_at": {
+ "name": "last_run_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_success_at": {
+ "name": "last_success_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "data_drains_org_idx": {
+ "name": "data_drains_org_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "data_drains_due_idx": {
+ "name": "data_drains_due_idx",
+ "columns": [
+ {
+ "expression": "enabled",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "last_run_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "data_drains_org_name_unique": {
+ "name": "data_drains_org_name_unique",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "data_drains_organization_id_organization_id_fk": {
+ "name": "data_drains_organization_id_organization_id_fk",
+ "tableFrom": "data_drains",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "data_drains_created_by_user_id_fk": {
+ "name": "data_drains_created_by_user_id_fk",
+ "tableFrom": "data_drains",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.docs_embeddings": {
+ "name": "docs_embeddings",
+ "schema": "",
+ "columns": {
+ "chunk_id": {
+ "name": "chunk_id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "chunk_text": {
+ "name": "chunk_text",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_document": {
+ "name": "source_document",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_link": {
+ "name": "source_link",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "header_text": {
+ "name": "header_text",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "header_level": {
+ "name": "header_level",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token_count": {
+ "name": "token_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "embedding": {
+ "name": "embedding",
+ "type": "vector(1536)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "embedding_model": {
+ "name": "embedding_model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'text-embedding-3-small'"
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "chunk_text_tsv": {
+ "name": "chunk_text_tsv",
+ "type": "tsvector",
+ "primaryKey": false,
+ "notNull": false,
+ "generated": {
+ "as": "to_tsvector('english', \"docs_embeddings\".\"chunk_text\")",
+ "type": "stored"
+ }
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "docs_emb_source_document_idx": {
+ "name": "docs_emb_source_document_idx",
+ "columns": [
+ {
+ "expression": "source_document",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "docs_emb_header_level_idx": {
+ "name": "docs_emb_header_level_idx",
+ "columns": [
+ {
+ "expression": "header_level",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "docs_emb_source_header_idx": {
+ "name": "docs_emb_source_header_idx",
+ "columns": [
+ {
+ "expression": "source_document",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "header_level",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "docs_emb_model_idx": {
+ "name": "docs_emb_model_idx",
+ "columns": [
+ {
+ "expression": "embedding_model",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "docs_emb_created_at_idx": {
+ "name": "docs_emb_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "docs_embedding_vector_hnsw_idx": {
+ "name": "docs_embedding_vector_hnsw_idx",
+ "columns": [
+ {
+ "expression": "embedding",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last",
+ "opclass": "vector_cosine_ops"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "hnsw",
+ "with": {
+ "m": 16,
+ "ef_construction": 64
+ }
+ },
+ "docs_emb_metadata_gin_idx": {
+ "name": "docs_emb_metadata_gin_idx",
+ "columns": [
+ {
+ "expression": "metadata",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ },
+ "docs_emb_chunk_text_fts_idx": {
+ "name": "docs_emb_chunk_text_fts_idx",
+ "columns": [
+ {
+ "expression": "chunk_text_tsv",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "docs_embedding_not_null_check": {
+ "name": "docs_embedding_not_null_check",
+ "value": "\"embedding\" IS NOT NULL"
+ },
+ "docs_header_level_check": {
+ "name": "docs_header_level_check",
+ "value": "\"header_level\" >= 1 AND \"header_level\" <= 6"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.document": {
+ "name": "document",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "knowledge_base_id": {
+ "name": "knowledge_base_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "filename": {
+ "name": "filename",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_url": {
+ "name": "file_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "storage_key": {
+ "name": "storage_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "file_size": {
+ "name": "file_size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "mime_type": {
+ "name": "mime_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "chunk_count": {
+ "name": "chunk_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "token_count": {
+ "name": "token_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "character_count": {
+ "name": "character_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "processing_status": {
+ "name": "processing_status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "processing_attempts": {
+ "name": "processing_attempts",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "processing_queued_at": {
+ "name": "processing_queued_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "processing_queue_token": {
+ "name": "processing_queue_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "processing_started_at": {
+ "name": "processing_started_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "processing_deferred_until": {
+ "name": "processing_deferred_until",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "processing_completed_at": {
+ "name": "processing_completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "processing_error": {
+ "name": "processing_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "processing_recovery_after": {
+ "name": "processing_recovery_after",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "archived_at": {
+ "name": "archived_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_excluded": {
+ "name": "user_excluded",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "tag1": {
+ "name": "tag1",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag2": {
+ "name": "tag2",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag3": {
+ "name": "tag3",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag4": {
+ "name": "tag4",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag5": {
+ "name": "tag5",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag6": {
+ "name": "tag6",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag7": {
+ "name": "tag7",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "number1": {
+ "name": "number1",
+ "type": "double precision",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "number2": {
+ "name": "number2",
+ "type": "double precision",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "number3": {
+ "name": "number3",
+ "type": "double precision",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "number4": {
+ "name": "number4",
+ "type": "double precision",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "number5": {
+ "name": "number5",
+ "type": "double precision",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "date1": {
+ "name": "date1",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "date2": {
+ "name": "date2",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "boolean1": {
+ "name": "boolean1",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "boolean2": {
+ "name": "boolean2",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "boolean3": {
+ "name": "boolean3",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "connector_id": {
+ "name": "connector_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "external_id": {
+ "name": "external_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "content_hash": {
+ "name": "content_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source_url": {
+ "name": "source_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "secret_provenance_version": {
+ "name": "secret_provenance_version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "uploaded_by": {
+ "name": "uploaded_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "acl": {
+ "name": "acl",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{ws}'::text[]"
+ },
+ "acl_requirements": {
+ "name": "acl_requirements",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "acl_verified_at": {
+ "name": "acl_verified_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source_modified_at": {
+ "name": "source_modified_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source_seen_at": {
+ "name": "source_seen_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "uploaded_at": {
+ "name": "uploaded_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "doc_kb_id_idx": {
+ "name": "doc_kb_id_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_acl_gin_idx": {
+ "name": "doc_acl_gin_idx",
+ "columns": [
+ {
+ "expression": "acl",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last",
+ "opclass": "array_ops"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"document\".\"deleted_at\" IS NULL",
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ },
+ "doc_filename_idx": {
+ "name": "doc_filename_idx",
+ "columns": [
+ {
+ "expression": "filename",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_processing_status_idx": {
+ "name": "doc_processing_status_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "processing_status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_processing_recovery_idx": {
+ "name": "doc_processing_recovery_idx",
+ "columns": [
+ {
+ "expression": "uploaded_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"document\".\"processing_status\" IN ('pending', 'processing', 'failed') AND \"document\".\"connector_id\" IS NOT NULL AND \"document\".\"content_hash\" IS NOT NULL AND \"document\".\"storage_key\" IS NOT NULL AND \"document\".\"user_excluded\" = false AND \"document\".\"archived_at\" IS NULL AND \"document\".\"deleted_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_connector_external_id_idx": {
+ "name": "doc_connector_external_id_idx",
+ "columns": [
+ {
+ "expression": "connector_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "external_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"document\".\"deleted_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_connector_source_lookup_idx": {
+ "name": "doc_connector_source_lookup_idx",
+ "columns": [
+ {
+ "expression": "connector_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "external_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_connector_reconciliation_idx": {
+ "name": "doc_connector_reconciliation_idx",
+ "columns": [
+ {
+ "expression": "connector_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "COALESCE(\"source_seen_at\", '-infinity'::timestamp)",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"document\".\"user_excluded\" = false AND \"document\".\"archived_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_active_kb_token_count_idx": {
+ "name": "doc_active_kb_token_count_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "token_count",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"document\".\"user_excluded\" = false AND \"document\".\"archived_at\" IS NULL AND \"document\".\"deleted_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_storage_key_idx": {
+ "name": "doc_storage_key_idx",
+ "columns": [
+ {
+ "expression": "storage_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"document\".\"storage_key\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_archived_at_partial_idx": {
+ "name": "doc_archived_at_partial_idx",
+ "columns": [
+ {
+ "expression": "archived_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"document\".\"archived_at\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_deleted_at_partial_idx": {
+ "name": "doc_deleted_at_partial_idx",
+ "columns": [
+ {
+ "expression": "deleted_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"document\".\"deleted_at\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_kb_tag1_lower_idx": {
+ "name": "doc_kb_tag1_lower_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "lower(\"tag1\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_kb_tag2_lower_idx": {
+ "name": "doc_kb_tag2_lower_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "lower(\"tag2\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_kb_tag3_lower_idx": {
+ "name": "doc_kb_tag3_lower_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "lower(\"tag3\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_kb_tag4_lower_idx": {
+ "name": "doc_kb_tag4_lower_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "lower(\"tag4\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_kb_tag5_lower_idx": {
+ "name": "doc_kb_tag5_lower_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "lower(\"tag5\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_kb_tag6_lower_idx": {
+ "name": "doc_kb_tag6_lower_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "lower(\"tag6\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_kb_tag7_lower_idx": {
+ "name": "doc_kb_tag7_lower_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "lower(\"tag7\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_number1_idx": {
+ "name": "doc_number1_idx",
+ "columns": [
+ {
+ "expression": "number1",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_number2_idx": {
+ "name": "doc_number2_idx",
+ "columns": [
+ {
+ "expression": "number2",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_number3_idx": {
+ "name": "doc_number3_idx",
+ "columns": [
+ {
+ "expression": "number3",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_number4_idx": {
+ "name": "doc_number4_idx",
+ "columns": [
+ {
+ "expression": "number4",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_number5_idx": {
+ "name": "doc_number5_idx",
+ "columns": [
+ {
+ "expression": "number5",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_date1_idx": {
+ "name": "doc_date1_idx",
+ "columns": [
+ {
+ "expression": "date1",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_date2_idx": {
+ "name": "doc_date2_idx",
+ "columns": [
+ {
+ "expression": "date2",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_boolean1_idx": {
+ "name": "doc_boolean1_idx",
+ "columns": [
+ {
+ "expression": "boolean1",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_boolean2_idx": {
+ "name": "doc_boolean2_idx",
+ "columns": [
+ {
+ "expression": "boolean2",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_boolean3_idx": {
+ "name": "doc_boolean3_idx",
+ "columns": [
+ {
+ "expression": "boolean3",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "document_knowledge_base_id_knowledge_base_id_fk": {
+ "name": "document_knowledge_base_id_knowledge_base_id_fk",
+ "tableFrom": "document",
+ "tableTo": "knowledge_base",
+ "columnsFrom": ["knowledge_base_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "document_connector_id_knowledge_connector_id_fk": {
+ "name": "document_connector_id_knowledge_connector_id_fk",
+ "tableFrom": "document",
+ "tableTo": "knowledge_connector",
+ "columnsFrom": ["connector_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "document_uploaded_by_user_id_fk": {
+ "name": "document_uploaded_by_user_id_fk",
+ "tableFrom": "document",
+ "tableTo": "user",
+ "columnsFrom": ["uploaded_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "doc_acl_token_shape_check": {
+ "name": "doc_acl_token_shape_check",
+ "value": "array_position(\"document\".\"acl\", NULL) IS NULL AND (cardinality(\"document\".\"acl\") = 0 OR (cardinality(\"document\".\"acl\") = array_length(string_to_array(array_to_string(\"document\".\"acl\", E'\\n'), E'\\n'), 1) AND array_to_string(\"document\".\"acl\", E'\\n') ~ '^((ws|pub|link|u:[^\\nA-Z]+@[^\\nA-Z]+|[gs]:[^\\n:]+:[^\\n:]+:[^\\n]+)(\\n(ws|pub|link|u:[^\\nA-Z]+@[^\\nA-Z]+|[gs]:[^\\n:]+:[^\\n:]+:[^\\n]+))*)$'))"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.document_secret_provenance": {
+ "name": "document_secret_provenance",
+ "schema": "",
+ "columns": {
+ "document_id": {
+ "name": "document_id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "source_hash": {
+ "name": "source_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "entries": {
+ "name": "entries",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "document_secret_provenance_document_id_document_id_fk": {
+ "name": "document_secret_provenance_document_id_document_id_fk",
+ "tableFrom": "document_secret_provenance",
+ "tableTo": "document",
+ "columnsFrom": ["document_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "document_secret_provenance_status_check": {
+ "name": "document_secret_provenance_status_check",
+ "value": "\"document_secret_provenance\".\"status\" IN ('exact', 'unknown')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.embedding": {
+ "name": "embedding",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "knowledge_base_id": {
+ "name": "knowledge_base_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "document_id": {
+ "name": "document_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "chunk_index": {
+ "name": "chunk_index",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "chunk_hash": {
+ "name": "chunk_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "secret_provenance_version": {
+ "name": "secret_provenance_version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "content_length": {
+ "name": "content_length",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token_count": {
+ "name": "token_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "embedding": {
+ "name": "embedding",
+ "type": "vector(1536)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "embedding_384": {
+ "name": "embedding_384",
+ "type": "vector(384)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "embedding_768": {
+ "name": "embedding_768",
+ "type": "vector(768)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "embedding_1024": {
+ "name": "embedding_1024",
+ "type": "vector(1024)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "embedding_3072": {
+ "name": "embedding_3072",
+ "type": "vector(3072)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "embedding_model": {
+ "name": "embedding_model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'text-embedding-3-small'"
+ },
+ "start_offset": {
+ "name": "start_offset",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "end_offset": {
+ "name": "end_offset",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tag1": {
+ "name": "tag1",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag2": {
+ "name": "tag2",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag3": {
+ "name": "tag3",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag4": {
+ "name": "tag4",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag5": {
+ "name": "tag5",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag6": {
+ "name": "tag6",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag7": {
+ "name": "tag7",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "number1": {
+ "name": "number1",
+ "type": "double precision",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "number2": {
+ "name": "number2",
+ "type": "double precision",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "number3": {
+ "name": "number3",
+ "type": "double precision",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "number4": {
+ "name": "number4",
+ "type": "double precision",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "number5": {
+ "name": "number5",
+ "type": "double precision",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "date1": {
+ "name": "date1",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "date2": {
+ "name": "date2",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "boolean1": {
+ "name": "boolean1",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "boolean2": {
+ "name": "boolean2",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "boolean3": {
+ "name": "boolean3",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "content_tsv": {
+ "name": "content_tsv",
+ "type": "tsvector",
+ "primaryKey": false,
+ "notNull": false,
+ "generated": {
+ "as": "to_tsvector('english', \"embedding\".\"content\")",
+ "type": "stored"
+ }
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "emb_kb_id_idx": {
+ "name": "emb_kb_id_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_doc_id_idx": {
+ "name": "emb_doc_id_idx",
+ "columns": [
+ {
+ "expression": "document_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_doc_chunk_idx": {
+ "name": "emb_doc_chunk_idx",
+ "columns": [
+ {
+ "expression": "document_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "chunk_index",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_kb_model_idx": {
+ "name": "emb_kb_model_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "embedding_model",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_kb_enabled_idx": {
+ "name": "emb_kb_enabled_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "enabled",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_doc_enabled_idx": {
+ "name": "emb_doc_enabled_idx",
+ "columns": [
+ {
+ "expression": "document_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "enabled",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "embedding_vector_hnsw_idx": {
+ "name": "embedding_vector_hnsw_idx",
+ "columns": [
+ {
+ "expression": "embedding",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last",
+ "opclass": "vector_cosine_ops"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "hnsw",
+ "with": {
+ "m": 16,
+ "ef_construction": 64
+ }
+ },
+ "embedding_384_vector_hnsw_idx": {
+ "name": "embedding_384_vector_hnsw_idx",
+ "columns": [
+ {
+ "expression": "embedding_384",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last",
+ "opclass": "vector_cosine_ops"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "hnsw",
+ "with": {
+ "m": 16,
+ "ef_construction": 64
+ }
+ },
+ "embedding_768_vector_hnsw_idx": {
+ "name": "embedding_768_vector_hnsw_idx",
+ "columns": [
+ {
+ "expression": "embedding_768",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last",
+ "opclass": "vector_cosine_ops"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "hnsw",
+ "with": {
+ "m": 16,
+ "ef_construction": 64
+ }
+ },
+ "embedding_1024_vector_hnsw_idx": {
+ "name": "embedding_1024_vector_hnsw_idx",
+ "columns": [
+ {
+ "expression": "embedding_1024",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last",
+ "opclass": "vector_cosine_ops"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "hnsw",
+ "with": {
+ "m": 16,
+ "ef_construction": 64
+ }
+ },
+ "embedding_3072_vector_hnsw_idx": {
+ "name": "embedding_3072_vector_hnsw_idx",
+ "columns": [
+ {
+ "expression": "(\"embedding_3072\"::halfvec(3072)) halfvec_cosine_ops",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "hnsw",
+ "with": {
+ "m": 16,
+ "ef_construction": 64
+ }
+ },
+ "emb_kb_tag1_lower_idx": {
+ "name": "emb_kb_tag1_lower_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "lower(\"tag1\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_kb_tag2_lower_idx": {
+ "name": "emb_kb_tag2_lower_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "lower(\"tag2\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_kb_tag3_lower_idx": {
+ "name": "emb_kb_tag3_lower_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "lower(\"tag3\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_kb_tag4_lower_idx": {
+ "name": "emb_kb_tag4_lower_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "lower(\"tag4\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_kb_tag5_lower_idx": {
+ "name": "emb_kb_tag5_lower_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "lower(\"tag5\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_kb_tag6_lower_idx": {
+ "name": "emb_kb_tag6_lower_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "lower(\"tag6\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_kb_tag7_lower_idx": {
+ "name": "emb_kb_tag7_lower_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "lower(\"tag7\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_number1_idx": {
+ "name": "emb_number1_idx",
+ "columns": [
+ {
+ "expression": "number1",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_number2_idx": {
+ "name": "emb_number2_idx",
+ "columns": [
+ {
+ "expression": "number2",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_number3_idx": {
+ "name": "emb_number3_idx",
+ "columns": [
+ {
+ "expression": "number3",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_number4_idx": {
+ "name": "emb_number4_idx",
+ "columns": [
+ {
+ "expression": "number4",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_number5_idx": {
+ "name": "emb_number5_idx",
+ "columns": [
+ {
+ "expression": "number5",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_date1_idx": {
+ "name": "emb_date1_idx",
+ "columns": [
+ {
+ "expression": "date1",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_date2_idx": {
+ "name": "emb_date2_idx",
+ "columns": [
+ {
+ "expression": "date2",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_boolean1_idx": {
+ "name": "emb_boolean1_idx",
+ "columns": [
+ {
+ "expression": "boolean1",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_boolean2_idx": {
+ "name": "emb_boolean2_idx",
+ "columns": [
+ {
+ "expression": "boolean2",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_boolean3_idx": {
+ "name": "emb_boolean3_idx",
+ "columns": [
+ {
+ "expression": "boolean3",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_content_fts_idx": {
+ "name": "emb_content_fts_idx",
+ "columns": [
+ {
+ "expression": "content_tsv",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "embedding_knowledge_base_id_knowledge_base_id_fk": {
+ "name": "embedding_knowledge_base_id_knowledge_base_id_fk",
+ "tableFrom": "embedding",
+ "tableTo": "knowledge_base",
+ "columnsFrom": ["knowledge_base_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "embedding_document_id_document_id_fk": {
+ "name": "embedding_document_id_document_id_fk",
+ "tableFrom": "embedding",
+ "tableTo": "document",
+ "columnsFrom": ["document_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "embedding_width_check": {
+ "name": "embedding_width_check",
+ "value": "num_nonnulls(\"embedding\", \"embedding_384\", \"embedding_768\", \"embedding_1024\", \"embedding_3072\") = 1"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.embedding_secret_provenance": {
+ "name": "embedding_secret_provenance",
+ "schema": "",
+ "columns": {
+ "embedding_id": {
+ "name": "embedding_id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "content_hash": {
+ "name": "content_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "entries": {
+ "name": "entries",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "embedding_secret_provenance_embedding_id_embedding_id_fk": {
+ "name": "embedding_secret_provenance_embedding_id_embedding_id_fk",
+ "tableFrom": "embedding_secret_provenance",
+ "tableTo": "embedding",
+ "columnsFrom": ["embedding_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "embedding_secret_provenance_status_check": {
+ "name": "embedding_secret_provenance_status_check",
+ "value": "\"embedding_secret_provenance\".\"status\" IN ('exact', 'unknown')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.environment": {
+ "name": "environment",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "variables": {
+ "name": "variables",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "environment_user_id_user_id_fk": {
+ "name": "environment_user_id_user_id_fk",
+ "tableFrom": "environment",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "environment_user_id_unique": {
+ "name": "environment_user_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["user_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.execution_large_value_dependencies": {
+ "name": "execution_large_value_dependencies",
+ "schema": "",
+ "columns": {
+ "parent_key": {
+ "name": "parent_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "child_key": {
+ "name": "child_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "execution_large_value_dependencies_workspace_parent_key_idx": {
+ "name": "execution_large_value_dependencies_workspace_parent_key_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "parent_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "execution_large_value_dependencies_workspace_child_key_idx": {
+ "name": "execution_large_value_dependencies_workspace_child_key_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "child_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "execution_large_value_dependencies_workspace_id_workspace_id_fk": {
+ "name": "execution_large_value_dependencies_workspace_id_workspace_id_fk",
+ "tableFrom": "execution_large_value_dependencies",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "execution_large_value_dependencies_parent_key_child_key_pk": {
+ "name": "execution_large_value_dependencies_parent_key_child_key_pk",
+ "columns": ["parent_key", "child_key"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.execution_large_value_references": {
+ "name": "execution_large_value_references",
+ "schema": "",
+ "columns": {
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "execution_id": {
+ "name": "execution_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source": {
+ "name": "source",
+ "type": "execution_large_value_reference_source",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "execution_large_value_references_workspace_execution_source_idx": {
+ "name": "execution_large_value_references_workspace_execution_source_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "execution_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "source",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "execution_large_value_references_workflow_id_idx": {
+ "name": "execution_large_value_references_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "execution_large_value_references_workspace_id_workspace_id_fk": {
+ "name": "execution_large_value_references_workspace_id_workspace_id_fk",
+ "tableFrom": "execution_large_value_references",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "execution_large_value_references_workflow_id_workflow_id_fk": {
+ "name": "execution_large_value_references_workflow_id_workflow_id_fk",
+ "tableFrom": "execution_large_value_references",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "execution_large_value_references_key_execution_id_source_pk": {
+ "name": "execution_large_value_references_key_execution_id_source_pk",
+ "columns": ["key", "execution_id", "source"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.execution_large_values": {
+ "name": "execution_large_values",
+ "schema": "",
+ "columns": {
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "owner_execution_id": {
+ "name": "owner_execution_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "size": {
+ "name": "size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "execution_large_values_owner_execution_id_idx": {
+ "name": "execution_large_values_owner_execution_id_idx",
+ "columns": [
+ {
+ "expression": "owner_execution_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "execution_large_values_cleanup_idx": {
+ "name": "execution_large_values_cleanup_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"execution_large_values\".\"deleted_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "execution_large_values_tombstone_cleanup_idx": {
+ "name": "execution_large_values_tombstone_cleanup_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "deleted_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"execution_large_values\".\"deleted_at\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "execution_large_values_workflow_id_idx": {
+ "name": "execution_large_values_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "execution_large_values_workspace_id_workspace_id_fk": {
+ "name": "execution_large_values_workspace_id_workspace_id_fk",
+ "tableFrom": "execution_large_values",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "execution_large_values_workflow_id_workflow_id_fk": {
+ "name": "execution_large_values_workflow_id_workflow_id_fk",
+ "tableFrom": "execution_large_values",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.folder": {
+ "name": "folder",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "resource_type": {
+ "name": "resource_type",
+ "type": "folder_resource_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "parent_id": {
+ "name": "parent_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "locked": {
+ "name": "locked",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "sort_order": {
+ "name": "sort_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "folder_user_idx": {
+ "name": "folder_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "folder_workspace_resource_parent_idx": {
+ "name": "folder_workspace_resource_parent_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "resource_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "parent_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "folder_parent_sort_idx": {
+ "name": "folder_parent_sort_idx",
+ "columns": [
+ {
+ "expression": "parent_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "sort_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "folder_deleted_at_idx": {
+ "name": "folder_deleted_at_idx",
+ "columns": [
+ {
+ "expression": "deleted_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "folder_workspace_deleted_partial_idx": {
+ "name": "folder_workspace_deleted_partial_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "deleted_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"folder\".\"deleted_at\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "folder_workspace_resource_parent_name_active_unique": {
+ "name": "folder_workspace_resource_parent_name_active_unique",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "resource_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "coalesce(\"parent_id\", '')",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"folder\".\"deleted_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "folder_user_id_user_id_fk": {
+ "name": "folder_user_id_user_id_fk",
+ "tableFrom": "folder",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "folder_workspace_id_workspace_id_fk": {
+ "name": "folder_workspace_id_workspace_id_fk",
+ "tableFrom": "folder",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "folder_parent_id_folder_id_fk": {
+ "name": "folder_parent_id_folder_id_fk",
+ "tableFrom": "folder",
+ "tableTo": "folder",
+ "columnsFrom": ["parent_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.idempotency_key": {
+ "name": "idempotency_key",
+ "schema": "",
+ "columns": {
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "result": {
+ "name": "result",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "idempotency_key_created_at_idx": {
+ "name": "idempotency_key_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.invitation": {
+ "name": "invitation",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "kind": {
+ "name": "kind",
+ "type": "invitation_kind",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'organization'"
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "inviter_id": {
+ "name": "inviter_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "membership_intent": {
+ "name": "membership_intent",
+ "type": "invitation_membership_intent",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'internal'"
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "invitation_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "invitation_email_idx": {
+ "name": "invitation_email_idx",
+ "columns": [
+ {
+ "expression": "email",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "invitation_organization_id_idx": {
+ "name": "invitation_organization_id_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "invitation_status_idx": {
+ "name": "invitation_status_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "invitation_pending_email_org_unique": {
+ "name": "invitation_pending_email_org_unique",
+ "columns": [
+ {
+ "expression": "email",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"invitation\".\"status\" = 'pending' AND \"invitation\".\"organization_id\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "invitation_inviter_id_user_id_fk": {
+ "name": "invitation_inviter_id_user_id_fk",
+ "tableFrom": "invitation",
+ "tableTo": "user",
+ "columnsFrom": ["inviter_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "invitation_organization_id_organization_id_fk": {
+ "name": "invitation_organization_id_organization_id_fk",
+ "tableFrom": "invitation",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "invitation_token_unique": {
+ "name": "invitation_token_unique",
+ "nullsNotDistinct": false,
+ "columns": ["token"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.invitation_workspace_grant": {
+ "name": "invitation_workspace_grant",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "invitation_id": {
+ "name": "invitation_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "permission": {
+ "name": "permission",
+ "type": "permission_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "invitation_workspace_grant_unique": {
+ "name": "invitation_workspace_grant_unique",
+ "columns": [
+ {
+ "expression": "invitation_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "invitation_workspace_grant_workspace_id_idx": {
+ "name": "invitation_workspace_grant_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "invitation_workspace_grant_invitation_id_invitation_id_fk": {
+ "name": "invitation_workspace_grant_invitation_id_invitation_id_fk",
+ "tableFrom": "invitation_workspace_grant",
+ "tableTo": "invitation",
+ "columnsFrom": ["invitation_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "invitation_workspace_grant_workspace_id_workspace_id_fk": {
+ "name": "invitation_workspace_grant_workspace_id_workspace_id_fk",
+ "tableFrom": "invitation_workspace_grant",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.job_execution_logs": {
+ "name": "job_execution_logs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "schedule_id": {
+ "name": "schedule_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "execution_id": {
+ "name": "execution_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "level": {
+ "name": "level",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'running'"
+ },
+ "trigger": {
+ "name": "trigger",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "ended_at": {
+ "name": "ended_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "total_duration_ms": {
+ "name": "total_duration_ms",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "execution_data": {
+ "name": "execution_data",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "cost": {
+ "name": "cost",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "job_execution_logs_schedule_id_idx": {
+ "name": "job_execution_logs_schedule_id_idx",
+ "columns": [
+ {
+ "expression": "schedule_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "job_execution_logs_workspace_started_at_idx": {
+ "name": "job_execution_logs_workspace_started_at_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "job_execution_logs_workspace_ended_at_id_idx": {
+ "name": "job_execution_logs_workspace_ended_at_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "date_trunc('milliseconds', \"ended_at\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "job_execution_logs_execution_id_unique": {
+ "name": "job_execution_logs_execution_id_unique",
+ "columns": [
+ {
+ "expression": "execution_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "job_execution_logs_trigger_idx": {
+ "name": "job_execution_logs_trigger_idx",
+ "columns": [
+ {
+ "expression": "trigger",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "job_execution_logs_schedule_id_workflow_schedule_id_fk": {
+ "name": "job_execution_logs_schedule_id_workflow_schedule_id_fk",
+ "tableFrom": "job_execution_logs",
+ "tableTo": "workflow_schedule",
+ "columnsFrom": ["schedule_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "job_execution_logs_workspace_id_workspace_id_fk": {
+ "name": "job_execution_logs_workspace_id_workspace_id_fk",
+ "tableFrom": "job_execution_logs",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.knowledge_base": {
+ "name": "knowledge_base",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "folder_id": {
+ "name": "folder_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_search_index": {
+ "name": "is_search_index",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "token_count": {
+ "name": "token_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "embedding_model": {
+ "name": "embedding_model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'text-embedding-3-small'"
+ },
+ "embedding_dimension": {
+ "name": "embedding_dimension",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1536
+ },
+ "chunking_config": {
+ "name": "chunking_config",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{\"maxSize\": 1024, \"minSize\": 1, \"overlap\": 200}'"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "kb_organization_id_idx": {
+ "name": "kb_organization_id_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kb_organization_search_index_unique": {
+ "name": "kb_organization_search_index_unique",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"knowledge_base\".\"is_search_index\" = true AND \"knowledge_base\".\"deleted_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kb_organization_name_active_unique": {
+ "name": "kb_organization_name_active_unique",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"knowledge_base\".\"deleted_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kb_user_id_idx": {
+ "name": "kb_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kb_workspace_id_idx": {
+ "name": "kb_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kb_user_workspace_idx": {
+ "name": "kb_user_workspace_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kb_folder_id_idx": {
+ "name": "kb_folder_id_idx",
+ "columns": [
+ {
+ "expression": "folder_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kb_deleted_at_idx": {
+ "name": "kb_deleted_at_idx",
+ "columns": [
+ {
+ "expression": "deleted_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kb_workspace_deleted_partial_idx": {
+ "name": "kb_workspace_deleted_partial_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "deleted_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"knowledge_base\".\"deleted_at\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kb_workspace_name_active_unique": {
+ "name": "kb_workspace_name_active_unique",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"knowledge_base\".\"deleted_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kb_workspace_search_index_unique": {
+ "name": "kb_workspace_search_index_unique",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"knowledge_base\".\"is_search_index\" = true AND \"knowledge_base\".\"deleted_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "knowledge_base_user_id_user_id_fk": {
+ "name": "knowledge_base_user_id_user_id_fk",
+ "tableFrom": "knowledge_base",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knowledge_base_workspace_id_workspace_id_fk": {
+ "name": "knowledge_base_workspace_id_workspace_id_fk",
+ "tableFrom": "knowledge_base",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knowledge_base_organization_id_organization_id_fk": {
+ "name": "knowledge_base_organization_id_organization_id_fk",
+ "tableFrom": "knowledge_base",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knowledge_base_folder_id_folder_id_fk": {
+ "name": "knowledge_base_folder_id_folder_id_fk",
+ "tableFrom": "knowledge_base",
+ "tableTo": "folder",
+ "columnsFrom": ["folder_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "kb_owner_check": {
+ "name": "kb_owner_check",
+ "value": "num_nonnulls(\"knowledge_base\".\"workspace_id\", \"knowledge_base\".\"organization_id\") = 1"
+ },
+ "kb_organization_search_index_check": {
+ "name": "kb_organization_search_index_check",
+ "value": "\"knowledge_base\".\"organization_id\" IS NULL OR \"knowledge_base\".\"is_search_index\""
+ },
+ "kb_organization_folder_check": {
+ "name": "kb_organization_folder_check",
+ "value": "\"knowledge_base\".\"organization_id\" IS NULL OR \"knowledge_base\".\"folder_id\" IS NULL"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.knowledge_base_tag_definitions": {
+ "name": "knowledge_base_tag_definitions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "knowledge_base_id": {
+ "name": "knowledge_base_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tag_slot": {
+ "name": "tag_slot",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "display_name": {
+ "name": "display_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "field_type": {
+ "name": "field_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'text'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "kb_tag_definitions_kb_slot_idx": {
+ "name": "kb_tag_definitions_kb_slot_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "tag_slot",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kb_tag_definitions_kb_display_name_idx": {
+ "name": "kb_tag_definitions_kb_display_name_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "display_name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kb_tag_definitions_kb_id_idx": {
+ "name": "kb_tag_definitions_kb_id_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk": {
+ "name": "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk",
+ "tableFrom": "knowledge_base_tag_definitions",
+ "tableTo": "knowledge_base",
+ "columnsFrom": ["knowledge_base_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.knowledge_connector": {
+ "name": "knowledge_connector",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "knowledge_base_id": {
+ "name": "knowledge_base_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "connector_type": {
+ "name": "connector_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "credential_id": {
+ "name": "credential_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "encrypted_api_key": {
+ "name": "encrypted_api_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source_config": {
+ "name": "source_config",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "sync_mode": {
+ "name": "sync_mode",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'full'"
+ },
+ "sync_interval_minutes": {
+ "name": "sync_interval_minutes",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1440
+ },
+ "access_mode": {
+ "name": "access_mode",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'workspace'"
+ },
+ "credential_group_id": {
+ "name": "credential_group_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "credential_group_option_id": {
+ "name": "credential_group_option_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "member_sync_status": {
+ "name": "member_sync_status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'idle'"
+ },
+ "member_sync_lock_token": {
+ "name": "member_sync_lock_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "member_sync_lock_lease_at": {
+ "name": "member_sync_lock_lease_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "next_member_sync_at": {
+ "name": "next_member_sync_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_member_sync_at": {
+ "name": "last_member_sync_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_member_sync_error": {
+ "name": "last_member_sync_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "member_sync_consecutive_failures": {
+ "name": "member_sync_consecutive_failures",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "access_rewrite_pending": {
+ "name": "access_rewrite_pending",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'active'"
+ },
+ "last_sync_at": {
+ "name": "last_sync_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_sync_error": {
+ "name": "last_sync_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_sync_doc_count": {
+ "name": "last_sync_doc_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "listing_checkpoint": {
+ "name": "listing_checkpoint",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "directory_checkpoint": {
+ "name": "directory_checkpoint",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "next_sync_at": {
+ "name": "next_sync_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "next_directory_sync_at": {
+ "name": "next_directory_sync_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "consecutive_failures": {
+ "name": "consecutive_failures",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "sync_lock_token": {
+ "name": "sync_lock_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sync_lock_lease_at": {
+ "name": "sync_lock_lease_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "archived_at": {
+ "name": "archived_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "kc_knowledge_base_id_idx": {
+ "name": "kc_knowledge_base_id_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kc_status_next_sync_idx": {
+ "name": "kc_status_next_sync_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "next_sync_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kc_archived_at_partial_idx": {
+ "name": "kc_archived_at_partial_idx",
+ "columns": [
+ {
+ "expression": "archived_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"knowledge_connector\".\"archived_at\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kc_deleted_at_partial_idx": {
+ "name": "kc_deleted_at_partial_idx",
+ "columns": [
+ {
+ "expression": "deleted_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"knowledge_connector\".\"deleted_at\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kc_member_sync_due_idx": {
+ "name": "kc_member_sync_due_idx",
+ "columns": [
+ {
+ "expression": "member_sync_status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "next_member_sync_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"knowledge_connector\".\"access_mode\" = 'members' AND \"knowledge_connector\".\"deleted_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kc_directory_sync_due_idx": {
+ "name": "kc_directory_sync_due_idx",
+ "columns": [
+ {
+ "expression": "next_directory_sync_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"knowledge_connector\".\"access_mode\" = 'admin' AND \"knowledge_connector\".\"deleted_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "knowledge_connector_knowledge_base_id_knowledge_base_id_fk": {
+ "name": "knowledge_connector_knowledge_base_id_knowledge_base_id_fk",
+ "tableFrom": "knowledge_connector",
+ "tableTo": "knowledge_base",
+ "columnsFrom": ["knowledge_base_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knowledge_connector_credential_group_id_credential_group_id_fk": {
+ "name": "knowledge_connector_credential_group_id_credential_group_id_fk",
+ "tableFrom": "knowledge_connector",
+ "tableTo": "credential_group",
+ "columnsFrom": ["credential_group_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "kc_access_mode_check": {
+ "name": "kc_access_mode_check",
+ "value": "\"knowledge_connector\".\"access_mode\" IN ('workspace', 'members', 'admin')"
+ },
+ "kc_member_sync_status_check": {
+ "name": "kc_member_sync_status_check",
+ "value": "\"knowledge_connector\".\"member_sync_status\" IN ('idle', 'pending', 'running', 'error', 'disabled')"
+ },
+ "kc_sync_lock_exclusive_check": {
+ "name": "kc_sync_lock_exclusive_check",
+ "value": "NOT (\"knowledge_connector\".\"sync_lock_token\" IS NOT NULL AND \"knowledge_connector\".\"member_sync_lock_token\" IS NOT NULL)"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.knowledge_connector_member": {
+ "name": "knowledge_connector_member",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "connector_id": {
+ "name": "connector_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "credential_id": {
+ "name": "credential_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "subject_token": {
+ "name": "subject_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'active'"
+ },
+ "consecutive_failures": {
+ "name": "consecutive_failures",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "next_attempt_at": {
+ "name": "next_attempt_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_started_at": {
+ "name": "last_started_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_complete_listing_at": {
+ "name": "last_complete_listing_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_listed_count": {
+ "name": "last_listed_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_error": {
+ "name": "last_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "member_synced_through": {
+ "name": "member_synced_through",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "change_cursor": {
+ "name": "change_cursor",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "listing_checkpoint": {
+ "name": "listing_checkpoint",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "suspended_at": {
+ "name": "suspended_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "kcm_organization_id_idx": {
+ "name": "kcm_organization_id_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kcm_connector_credential_unique": {
+ "name": "kcm_connector_credential_unique",
+ "columns": [
+ {
+ "expression": "connector_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "credential_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kcm_connector_queue_idx": {
+ "name": "kcm_connector_queue_idx",
+ "columns": [
+ {
+ "expression": "connector_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "next_attempt_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "first"
+ },
+ {
+ "expression": "last_started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "first"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kcm_credential_idx": {
+ "name": "kcm_credential_idx",
+ "columns": [
+ {
+ "expression": "credential_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "knowledge_connector_member_workspace_id_workspace_id_fk": {
+ "name": "knowledge_connector_member_workspace_id_workspace_id_fk",
+ "tableFrom": "knowledge_connector_member",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knowledge_connector_member_organization_id_organization_id_fk": {
+ "name": "knowledge_connector_member_organization_id_organization_id_fk",
+ "tableFrom": "knowledge_connector_member",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knowledge_connector_member_connector_id_knowledge_connector_id_fk": {
+ "name": "knowledge_connector_member_connector_id_knowledge_connector_id_fk",
+ "tableFrom": "knowledge_connector_member",
+ "tableTo": "knowledge_connector",
+ "columnsFrom": ["connector_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knowledge_connector_member_credential_id_credential_id_fk": {
+ "name": "knowledge_connector_member_credential_id_credential_id_fk",
+ "tableFrom": "knowledge_connector_member",
+ "tableTo": "credential",
+ "columnsFrom": ["credential_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "kcm_owner_check": {
+ "name": "kcm_owner_check",
+ "value": "num_nonnulls(\"knowledge_connector_member\".\"workspace_id\", \"knowledge_connector_member\".\"organization_id\") = 1"
+ },
+ "kcm_status_check": {
+ "name": "kcm_status_check",
+ "value": "\"knowledge_connector_member\".\"status\" IN ('active', 'suspended', 'disabled')"
+ },
+ "kcm_subject_token_shape_check": {
+ "name": "kcm_subject_token_shape_check",
+ "value": "\"knowledge_connector_member\".\"subject_token\" ~ '^s:[^:]+:[^:]+:.+$'"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.knowledge_connector_member_sync_log": {
+ "name": "knowledge_connector_member_sync_log",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "connector_id": {
+ "name": "connector_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "members_claimed": {
+ "name": "members_claimed",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "members_completed": {
+ "name": "members_completed",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "members_incomplete": {
+ "name": "members_incomplete",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "members_failed": {
+ "name": "members_failed",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "docs_listed": {
+ "name": "docs_listed",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "docs_added": {
+ "name": "docs_added",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "docs_updated": {
+ "name": "docs_updated",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "docs_unchanged": {
+ "name": "docs_unchanged",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "docs_hydrated_once": {
+ "name": "docs_hydrated_once",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "observations_added": {
+ "name": "observations_added",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "observations_removed": {
+ "name": "observations_removed",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "docs_tombstoned": {
+ "name": "docs_tombstoned",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "docs_resurrected": {
+ "name": "docs_resurrected",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "docs_purged": {
+ "name": "docs_purged",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "credentials_audited": {
+ "name": "credentials_audited",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "kcmsl_connector_started_at_idx": {
+ "name": "kcmsl_connector_started_at_idx",
+ "columns": [
+ {
+ "expression": "connector_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "\"started_at\" DESC",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kcmsl_started_at_partial_idx": {
+ "name": "kcmsl_started_at_partial_idx",
+ "columns": [
+ {
+ "expression": "started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"knowledge_connector_member_sync_log\".\"status\" = 'started'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "knowledge_connector_member_sync_log_connector_id_knowledge_connector_id_fk": {
+ "name": "knowledge_connector_member_sync_log_connector_id_knowledge_connector_id_fk",
+ "tableFrom": "knowledge_connector_member_sync_log",
+ "tableTo": "knowledge_connector",
+ "columnsFrom": ["connector_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "kcmsl_status_check": {
+ "name": "kcmsl_status_check",
+ "value": "\"knowledge_connector_member_sync_log\".\"status\" IN ('started', 'partial', 'completed', 'failed')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.knowledge_connector_sync_log": {
+ "name": "knowledge_connector_sync_log",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "connector_id": {
+ "name": "connector_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "docs_added": {
+ "name": "docs_added",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "docs_updated": {
+ "name": "docs_updated",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "docs_deleted": {
+ "name": "docs_deleted",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "docs_unchanged": {
+ "name": "docs_unchanged",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "docs_skipped": {
+ "name": "docs_skipped",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "docs_failed": {
+ "name": "docs_failed",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "listed_count": {
+ "name": "listed_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "kcsl_connector_started_at_idx": {
+ "name": "kcsl_connector_started_at_idx",
+ "columns": [
+ {
+ "expression": "connector_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "\"started_at\" DESC",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kcsl_started_at_partial_idx": {
+ "name": "kcsl_started_at_partial_idx",
+ "columns": [
+ {
+ "expression": "started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"knowledge_connector_sync_log\".\"status\" = 'started'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk": {
+ "name": "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk",
+ "tableFrom": "knowledge_connector_sync_log",
+ "tableTo": "knowledge_connector",
+ "columnsFrom": ["connector_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.knowledge_document_observation": {
+ "name": "knowledge_document_observation",
+ "schema": "",
+ "columns": {
+ "document_id": {
+ "name": "document_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "member_id": {
+ "name": "member_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "last_seen_at": {
+ "name": "last_seen_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "run_id": {
+ "name": "run_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "kdo_member_idx": {
+ "name": "kdo_member_idx",
+ "columns": [
+ {
+ "expression": "member_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "knowledge_document_observation_document_id_document_id_fk": {
+ "name": "knowledge_document_observation_document_id_document_id_fk",
+ "tableFrom": "knowledge_document_observation",
+ "tableTo": "document",
+ "columnsFrom": ["document_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knowledge_document_observation_member_id_knowledge_connector_member_id_fk": {
+ "name": "knowledge_document_observation_member_id_knowledge_connector_member_id_fk",
+ "tableFrom": "knowledge_document_observation",
+ "tableTo": "knowledge_connector_member",
+ "columnsFrom": ["member_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "knowledge_document_observation_document_id_member_id_pk": {
+ "name": "knowledge_document_observation_document_id_member_id_pk",
+ "columns": ["document_id", "member_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.knowledge_external_directory": {
+ "name": "knowledge_external_directory",
+ "schema": "",
+ "columns": {
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tenant_id": {
+ "name": "tenant_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "sync_lock_token": {
+ "name": "sync_lock_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sync_lock_lease_at": {
+ "name": "sync_lock_lease_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_started_at": {
+ "name": "last_started_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_complete_sync_at": {
+ "name": "last_complete_sync_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "ked_organization_id_idx": {
+ "name": "ked_organization_id_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "ked_workspace_identity_unique": {
+ "name": "ked_workspace_identity_unique",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "tenant_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "ked_organization_identity_unique": {
+ "name": "ked_organization_identity_unique",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "tenant_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "knowledge_external_directory_workspace_id_workspace_id_fk": {
+ "name": "knowledge_external_directory_workspace_id_workspace_id_fk",
+ "tableFrom": "knowledge_external_directory",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knowledge_external_directory_organization_id_organization_id_fk": {
+ "name": "knowledge_external_directory_organization_id_organization_id_fk",
+ "tableFrom": "knowledge_external_directory",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "ked_owner_check": {
+ "name": "ked_owner_check",
+ "value": "num_nonnulls(\"knowledge_external_directory\".\"workspace_id\", \"knowledge_external_directory\".\"organization_id\") = 1"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.knowledge_external_group": {
+ "name": "knowledge_external_group",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tenant_id": {
+ "name": "tenant_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "external_group_id": {
+ "name": "external_group_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "last_synced_at": {
+ "name": "last_synced_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "keg_organization_id_idx": {
+ "name": "keg_organization_id_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "keg_organization_identity_unique": {
+ "name": "keg_organization_identity_unique",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "tenant_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "external_group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "keg_organization_synced_idx": {
+ "name": "keg_organization_synced_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "last_synced_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "first"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "keg_identity_unique": {
+ "name": "keg_identity_unique",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "tenant_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "external_group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "keg_workspace_synced_idx": {
+ "name": "keg_workspace_synced_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "last_synced_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "first"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "knowledge_external_group_organization_id_organization_id_fk": {
+ "name": "knowledge_external_group_organization_id_organization_id_fk",
+ "tableFrom": "knowledge_external_group",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "keg_workspace_fk": {
+ "name": "keg_workspace_fk",
+ "tableFrom": "knowledge_external_group",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "keg_owner_check": {
+ "name": "keg_owner_check",
+ "value": "num_nonnulls(\"knowledge_external_group\".\"workspace_id\", \"knowledge_external_group\".\"organization_id\") = 1"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.knowledge_external_group_member": {
+ "name": "knowledge_external_group_member",
+ "schema": "",
+ "columns": {
+ "group_id": {
+ "name": "group_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "subject_token": {
+ "name": "subject_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "kegm_subject_token_idx": {
+ "name": "kegm_subject_token_idx",
+ "columns": [
+ {
+ "expression": "subject_token",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "kegm_group_fk": {
+ "name": "kegm_group_fk",
+ "tableFrom": "knowledge_external_group_member",
+ "tableTo": "knowledge_external_group",
+ "columnsFrom": ["group_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "knowledge_external_group_member_group_id_subject_token_pk": {
+ "name": "knowledge_external_group_member_group_id_subject_token_pk",
+ "columns": ["group_id", "subject_token"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mcp_server_oauth": {
+ "name": "mcp_server_oauth",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "mcp_server_id": {
+ "name": "mcp_server_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "client_information": {
+ "name": "client_information",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tokens": {
+ "name": "tokens",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "code_verifier": {
+ "name": "code_verifier",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "state": {
+ "name": "state",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "state_created_at": {
+ "name": "state_created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_refreshed_at": {
+ "name": "last_refreshed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "mcp_server_oauth_server_unique": {
+ "name": "mcp_server_oauth_server_unique",
+ "columns": [
+ {
+ "expression": "mcp_server_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "mcp_server_oauth_state_idx": {
+ "name": "mcp_server_oauth_state_idx",
+ "columns": [
+ {
+ "expression": "state",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk": {
+ "name": "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk",
+ "tableFrom": "mcp_server_oauth",
+ "tableTo": "mcp_servers",
+ "columnsFrom": ["mcp_server_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "mcp_server_oauth_user_id_user_id_fk": {
+ "name": "mcp_server_oauth_user_id_user_id_fk",
+ "tableFrom": "mcp_server_oauth",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "mcp_server_oauth_workspace_id_workspace_id_fk": {
+ "name": "mcp_server_oauth_workspace_id_workspace_id_fk",
+ "tableFrom": "mcp_server_oauth",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "mcp_server_oauth_organization_id_organization_id_fk": {
+ "name": "mcp_server_oauth_organization_id_organization_id_fk",
+ "tableFrom": "mcp_server_oauth",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "mcp_server_oauth_owner_check": {
+ "name": "mcp_server_oauth_owner_check",
+ "value": "num_nonnulls(\"mcp_server_oauth\".\"workspace_id\", \"mcp_server_oauth\".\"organization_id\") = 1"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.mcp_servers": {
+ "name": "mcp_servers",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "credential_group_id": {
+ "name": "credential_group_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "managed_connector_id": {
+ "name": "managed_connector_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "oauth_config_version": {
+ "name": "oauth_config_version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "transport": {
+ "name": "transport",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "auth_type": {
+ "name": "auth_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'headers'"
+ },
+ "oauth_client_id": {
+ "name": "oauth_client_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "oauth_client_secret": {
+ "name": "oauth_client_secret",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "headers": {
+ "name": "headers",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'{}'"
+ },
+ "timeout": {
+ "name": "timeout",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 30000
+ },
+ "retries": {
+ "name": "retries",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 3
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "last_connected": {
+ "name": "last_connected",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "connection_status": {
+ "name": "connection_status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'disconnected'"
+ },
+ "last_error": {
+ "name": "last_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status_config": {
+ "name": "status_config",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'{}'"
+ },
+ "tool_count": {
+ "name": "tool_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 0
+ },
+ "last_tools_refresh": {
+ "name": "last_tools_refresh",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "total_requests": {
+ "name": "total_requests",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 0
+ },
+ "last_used": {
+ "name": "last_used",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "mcp_servers_organization_id_idx": {
+ "name": "mcp_servers_organization_id_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "mcp_servers_workspace_enabled_idx": {
+ "name": "mcp_servers_workspace_enabled_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "enabled",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "mcp_servers_credential_group_idx": {
+ "name": "mcp_servers_credential_group_idx",
+ "columns": [
+ {
+ "expression": "credential_group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "mcp_servers_credential_group_managed_connector_unique": {
+ "name": "mcp_servers_credential_group_managed_connector_unique",
+ "columns": [
+ {
+ "expression": "credential_group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "managed_connector_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"mcp_servers\".\"credential_group_id\" IS NOT NULL AND \"mcp_servers\".\"managed_connector_id\" IS NOT NULL AND \"mcp_servers\".\"deleted_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "mcp_servers_workspace_deleted_partial_idx": {
+ "name": "mcp_servers_workspace_deleted_partial_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "deleted_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"mcp_servers\".\"deleted_at\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "mcp_servers_workspace_id_workspace_id_fk": {
+ "name": "mcp_servers_workspace_id_workspace_id_fk",
+ "tableFrom": "mcp_servers",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "mcp_servers_organization_id_organization_id_fk": {
+ "name": "mcp_servers_organization_id_organization_id_fk",
+ "tableFrom": "mcp_servers",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "mcp_servers_credential_group_id_credential_group_id_fk": {
+ "name": "mcp_servers_credential_group_id_credential_group_id_fk",
+ "tableFrom": "mcp_servers",
+ "tableTo": "credential_group",
+ "columnsFrom": ["credential_group_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "mcp_servers_created_by_user_id_fk": {
+ "name": "mcp_servers_created_by_user_id_fk",
+ "tableFrom": "mcp_servers",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "mcp_servers_owner_check": {
+ "name": "mcp_servers_owner_check",
+ "value": "num_nonnulls(\"mcp_servers\".\"workspace_id\", \"mcp_servers\".\"organization_id\") = 1"
+ },
+ "mcp_servers_organization_managed_check": {
+ "name": "mcp_servers_organization_managed_check",
+ "value": "\"mcp_servers\".\"organization_id\" IS NULL OR \"mcp_servers\".\"credential_group_id\" IS NOT NULL"
+ },
+ "mcp_servers_credential_group_managed_connector_check": {
+ "name": "mcp_servers_credential_group_managed_connector_check",
+ "value": "\"mcp_servers\".\"credential_group_id\" IS NULL OR \"mcp_servers\".\"managed_connector_id\" IS NOT NULL"
+ },
+ "mcp_servers_managed_connector_oauth_check": {
+ "name": "mcp_servers_managed_connector_oauth_check",
+ "value": "\"mcp_servers\".\"managed_connector_id\" IS NULL OR \"mcp_servers\".\"auth_type\" = 'oauth'"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.member": {
+ "name": "member",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "member_user_id_unique": {
+ "name": "member_user_id_unique",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "member_organization_id_idx": {
+ "name": "member_organization_id_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "member_user_id_user_id_fk": {
+ "name": "member_user_id_user_id_fk",
+ "tableFrom": "member",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "member_organization_id_organization_id_fk": {
+ "name": "member_organization_id_organization_id_fk",
+ "tableFrom": "member",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.memory": {
+ "name": "memory",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "data": {
+ "name": "data",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "secret_provenance_version": {
+ "name": "secret_provenance_version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "memory_key_idx": {
+ "name": "memory_key_idx",
+ "columns": [
+ {
+ "expression": "key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "memory_workspace_idx": {
+ "name": "memory_workspace_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "memory_workspace_key_idx": {
+ "name": "memory_workspace_key_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "memory_workspace_deleted_partial_idx": {
+ "name": "memory_workspace_deleted_partial_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "deleted_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"memory\".\"deleted_at\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "memory_workspace_id_workspace_id_fk": {
+ "name": "memory_workspace_id_workspace_id_fk",
+ "tableFrom": "memory",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.memory_secret_provenance": {
+ "name": "memory_secret_provenance",
+ "schema": "",
+ "columns": {
+ "memory_id": {
+ "name": "memory_id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "content_hash": {
+ "name": "content_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "entries": {
+ "name": "entries",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "memory_secret_provenance_memory_id_memory_id_fk": {
+ "name": "memory_secret_provenance_memory_id_memory_id_fk",
+ "tableFrom": "memory_secret_provenance",
+ "tableTo": "memory",
+ "columnsFrom": ["memory_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "memory_secret_provenance_status_check": {
+ "name": "memory_secret_provenance_status_check",
+ "value": "\"memory_secret_provenance\".\"status\" IN ('exact', 'unknown')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.mothership_inbox_allowed_sender": {
+ "name": "mothership_inbox_allowed_sender",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "label": {
+ "name": "label",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "added_by": {
+ "name": "added_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "inbox_sender_ws_email_idx": {
+ "name": "inbox_sender_ws_email_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "email",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk": {
+ "name": "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk",
+ "tableFrom": "mothership_inbox_allowed_sender",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "mothership_inbox_allowed_sender_added_by_user_id_fk": {
+ "name": "mothership_inbox_allowed_sender_added_by_user_id_fk",
+ "tableFrom": "mothership_inbox_allowed_sender",
+ "tableTo": "user",
+ "columnsFrom": ["added_by"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mothership_inbox_task": {
+ "name": "mothership_inbox_task",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "from_email": {
+ "name": "from_email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "from_name": {
+ "name": "from_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "subject": {
+ "name": "subject",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "body_preview": {
+ "name": "body_preview",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "body_text": {
+ "name": "body_text",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "body_html": {
+ "name": "body_html",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "email_message_id": {
+ "name": "email_message_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "in_reply_to": {
+ "name": "in_reply_to",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "response_message_id": {
+ "name": "response_message_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "agentmail_message_id": {
+ "name": "agentmail_message_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'received'"
+ },
+ "chat_id": {
+ "name": "chat_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "trigger_job_id": {
+ "name": "trigger_job_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "result_summary": {
+ "name": "result_summary",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rejection_reason": {
+ "name": "rejection_reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "has_attachments": {
+ "name": "has_attachments",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "cc_recipients": {
+ "name": "cc_recipients",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "processing_started_at": {
+ "name": "processing_started_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "inbox_task_ws_created_at_idx": {
+ "name": "inbox_task_ws_created_at_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "inbox_task_ws_status_idx": {
+ "name": "inbox_task_ws_status_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "inbox_task_response_msg_id_idx": {
+ "name": "inbox_task_response_msg_id_idx",
+ "columns": [
+ {
+ "expression": "response_message_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "inbox_task_email_msg_id_idx": {
+ "name": "inbox_task_email_msg_id_idx",
+ "columns": [
+ {
+ "expression": "email_message_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "mothership_inbox_task_workspace_id_workspace_id_fk": {
+ "name": "mothership_inbox_task_workspace_id_workspace_id_fk",
+ "tableFrom": "mothership_inbox_task",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "mothership_inbox_task_chat_id_copilot_chats_id_fk": {
+ "name": "mothership_inbox_task_chat_id_copilot_chats_id_fk",
+ "tableFrom": "mothership_inbox_task",
+ "tableTo": "copilot_chats",
+ "columnsFrom": ["chat_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mothership_inbox_webhook": {
+ "name": "mothership_inbox_webhook",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "webhook_id": {
+ "name": "webhook_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "secret": {
+ "name": "secret",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "mothership_inbox_webhook_workspace_id_workspace_id_fk": {
+ "name": "mothership_inbox_webhook_workspace_id_workspace_id_fk",
+ "tableFrom": "mothership_inbox_webhook",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "mothership_inbox_webhook_workspace_id_unique": {
+ "name": "mothership_inbox_webhook_workspace_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["workspace_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mothership_settings": {
+ "name": "mothership_settings",
+ "schema": "",
+ "columns": {
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "mcp_tool_refs": {
+ "name": "mcp_tool_refs",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "custom_tool_refs": {
+ "name": "custom_tool_refs",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "skill_refs": {
+ "name": "skill_refs",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "mothership_settings_workspace_id_workspace_id_fk": {
+ "name": "mothership_settings_workspace_id_workspace_id_fk",
+ "tableFrom": "mothership_settings",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.oauth_access_token": {
+ "name": "oauth_access_token",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "client_id": {
+ "name": "client_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "session_id": {
+ "name": "session_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reference_id": {
+ "name": "reference_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_id": {
+ "name": "refresh_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "scopes": {
+ "name": "scopes",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "resource": {
+ "name": "resource",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "oauth_access_token_client_id_idx": {
+ "name": "oauth_access_token_client_id_idx",
+ "columns": [
+ {
+ "expression": "client_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "oauth_access_token_session_id_idx": {
+ "name": "oauth_access_token_session_id_idx",
+ "columns": [
+ {
+ "expression": "session_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "oauth_access_token_refresh_id_idx": {
+ "name": "oauth_access_token_refresh_id_idx",
+ "columns": [
+ {
+ "expression": "refresh_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "oauth_access_token_user_client_idx": {
+ "name": "oauth_access_token_user_client_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "client_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "oauth_access_token_expires_at_idx": {
+ "name": "oauth_access_token_expires_at_idx",
+ "columns": [
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "oauth_access_token_client_id_oauth_client_client_id_fk": {
+ "name": "oauth_access_token_client_id_oauth_client_client_id_fk",
+ "tableFrom": "oauth_access_token",
+ "tableTo": "oauth_client",
+ "columnsFrom": ["client_id"],
+ "columnsTo": ["client_id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "oauth_access_token_session_id_session_id_fk": {
+ "name": "oauth_access_token_session_id_session_id_fk",
+ "tableFrom": "oauth_access_token",
+ "tableTo": "session",
+ "columnsFrom": ["session_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "oauth_access_token_user_id_user_id_fk": {
+ "name": "oauth_access_token_user_id_user_id_fk",
+ "tableFrom": "oauth_access_token",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "oauth_access_token_refresh_id_oauth_refresh_token_id_fk": {
+ "name": "oauth_access_token_refresh_id_oauth_refresh_token_id_fk",
+ "tableFrom": "oauth_access_token",
+ "tableTo": "oauth_refresh_token",
+ "columnsFrom": ["refresh_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "oauth_access_token_token_unique": {
+ "name": "oauth_access_token_token_unique",
+ "nullsNotDistinct": false,
+ "columns": ["token"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "oauth_access_token_search_resource_check": {
+ "name": "oauth_access_token_search_resource_check",
+ "value": "NOT ('search:read' = ANY(\"oauth_access_token\".\"scopes\")) OR \"oauth_access_token\".\"resource\" IS NOT NULL"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.oauth_client": {
+ "name": "oauth_client",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "client_id": {
+ "name": "client_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "client_secret": {
+ "name": "client_secret",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "disabled": {
+ "name": "disabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "skip_consent": {
+ "name": "skip_consent",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "enable_end_session": {
+ "name": "enable_end_session",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "subject_type": {
+ "name": "subject_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scopes": {
+ "name": "scopes",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "uri": {
+ "name": "uri",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "icon": {
+ "name": "icon",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "contacts": {
+ "name": "contacts",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tos": {
+ "name": "tos",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "policy": {
+ "name": "policy",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "software_id": {
+ "name": "software_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "software_version": {
+ "name": "software_version",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "software_statement": {
+ "name": "software_statement",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "redirect_uris": {
+ "name": "redirect_uris",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "post_logout_redirect_uris": {
+ "name": "post_logout_redirect_uris",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "token_endpoint_auth_method": {
+ "name": "token_endpoint_auth_method",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "grant_types": {
+ "name": "grant_types",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "response_types": {
+ "name": "response_types",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "public": {
+ "name": "public",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "require_pkce": {
+ "name": "require_pkce",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reference_id": {
+ "name": "reference_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "oauth_client_user_id_idx": {
+ "name": "oauth_client_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "oauth_client_user_id_user_id_fk": {
+ "name": "oauth_client_user_id_user_id_fk",
+ "tableFrom": "oauth_client",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "oauth_client_client_id_unique": {
+ "name": "oauth_client_client_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["client_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.oauth_consent": {
+ "name": "oauth_consent",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "client_id": {
+ "name": "client_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reference_id": {
+ "name": "reference_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scopes": {
+ "name": "scopes",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "oauth_consent_client_id_idx": {
+ "name": "oauth_consent_client_id_idx",
+ "columns": [
+ {
+ "expression": "client_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "oauth_consent_client_id_oauth_client_client_id_fk": {
+ "name": "oauth_consent_client_id_oauth_client_client_id_fk",
+ "tableFrom": "oauth_consent",
+ "tableTo": "oauth_client",
+ "columnsFrom": ["client_id"],
+ "columnsTo": ["client_id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "oauth_consent_user_id_user_id_fk": {
+ "name": "oauth_consent_user_id_user_id_fk",
+ "tableFrom": "oauth_consent",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "oauth_consent_user_client_reference_unique": {
+ "name": "oauth_consent_user_client_reference_unique",
+ "nullsNotDistinct": true,
+ "columns": ["user_id", "client_id", "reference_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.oauth_refresh_token": {
+ "name": "oauth_refresh_token",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "client_id": {
+ "name": "client_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "session_id": {
+ "name": "session_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reference_id": {
+ "name": "reference_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "revoked": {
+ "name": "revoked",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "auth_time": {
+ "name": "auth_time",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scopes": {
+ "name": "scopes",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "resource": {
+ "name": "resource",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "family_id": {
+ "name": "family_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "generation": {
+ "name": "generation",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "oauth_refresh_token_client_id_idx": {
+ "name": "oauth_refresh_token_client_id_idx",
+ "columns": [
+ {
+ "expression": "client_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "oauth_refresh_token_session_id_idx": {
+ "name": "oauth_refresh_token_session_id_idx",
+ "columns": [
+ {
+ "expression": "session_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "oauth_refresh_token_user_client_idx": {
+ "name": "oauth_refresh_token_user_client_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "client_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "oauth_refresh_token_expires_at_idx": {
+ "name": "oauth_refresh_token_expires_at_idx",
+ "columns": [
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "oauth_refresh_token_client_id_oauth_client_client_id_fk": {
+ "name": "oauth_refresh_token_client_id_oauth_client_client_id_fk",
+ "tableFrom": "oauth_refresh_token",
+ "tableTo": "oauth_client",
+ "columnsFrom": ["client_id"],
+ "columnsTo": ["client_id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "oauth_refresh_token_session_id_session_id_fk": {
+ "name": "oauth_refresh_token_session_id_session_id_fk",
+ "tableFrom": "oauth_refresh_token",
+ "tableTo": "session",
+ "columnsFrom": ["session_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "oauth_refresh_token_user_id_user_id_fk": {
+ "name": "oauth_refresh_token_user_id_user_id_fk",
+ "tableFrom": "oauth_refresh_token",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "oauth_refresh_token_family_id_oauth_token_family_id_fk": {
+ "name": "oauth_refresh_token_family_id_oauth_token_family_id_fk",
+ "tableFrom": "oauth_refresh_token",
+ "tableTo": "oauth_token_family",
+ "columnsFrom": ["family_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "oauth_refresh_token_token_unique": {
+ "name": "oauth_refresh_token_token_unique",
+ "nullsNotDistinct": false,
+ "columns": ["token"]
+ },
+ "oauth_refresh_token_family_generation_unique": {
+ "name": "oauth_refresh_token_family_generation_unique",
+ "nullsNotDistinct": false,
+ "columns": ["family_id", "generation"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "oauth_refresh_token_generation_check": {
+ "name": "oauth_refresh_token_generation_check",
+ "value": "\"oauth_refresh_token\".\"generation\" BETWEEN 0 AND 1000"
+ },
+ "oauth_refresh_token_search_resource_check": {
+ "name": "oauth_refresh_token_search_resource_check",
+ "value": "NOT ('search:read' = ANY(\"oauth_refresh_token\".\"scopes\")) OR \"oauth_refresh_token\".\"resource\" IS NOT NULL"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.oauth_token_family": {
+ "name": "oauth_token_family",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "client_id": {
+ "name": "client_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "session_id": {
+ "name": "session_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reference_id": {
+ "name": "reference_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "consent_id": {
+ "name": "consent_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "current_generation": {
+ "name": "current_generation",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "oauth_token_family_client_id_idx": {
+ "name": "oauth_token_family_client_id_idx",
+ "columns": [
+ {
+ "expression": "client_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "oauth_token_family_session_id_idx": {
+ "name": "oauth_token_family_session_id_idx",
+ "columns": [
+ {
+ "expression": "session_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "oauth_token_family_user_client_idx": {
+ "name": "oauth_token_family_user_client_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "client_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "oauth_token_family_consent_id_idx": {
+ "name": "oauth_token_family_consent_id_idx",
+ "columns": [
+ {
+ "expression": "consent_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "oauth_token_family_expires_at_idx": {
+ "name": "oauth_token_family_expires_at_idx",
+ "columns": [
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "oauth_token_family_client_id_oauth_client_client_id_fk": {
+ "name": "oauth_token_family_client_id_oauth_client_client_id_fk",
+ "tableFrom": "oauth_token_family",
+ "tableTo": "oauth_client",
+ "columnsFrom": ["client_id"],
+ "columnsTo": ["client_id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "oauth_token_family_session_id_session_id_fk": {
+ "name": "oauth_token_family_session_id_session_id_fk",
+ "tableFrom": "oauth_token_family",
+ "tableTo": "session",
+ "columnsFrom": ["session_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "oauth_token_family_user_id_user_id_fk": {
+ "name": "oauth_token_family_user_id_user_id_fk",
+ "tableFrom": "oauth_token_family",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "oauth_token_family_consent_id_oauth_consent_id_fk": {
+ "name": "oauth_token_family_consent_id_oauth_consent_id_fk",
+ "tableFrom": "oauth_token_family",
+ "tableTo": "oauth_consent",
+ "columnsFrom": ["consent_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "oauth_token_family_generation_check": {
+ "name": "oauth_token_family_generation_check",
+ "value": "\"oauth_token_family\".\"current_generation\" BETWEEN 0 AND 1000"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.organization": {
+ "name": "organization",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slug": {
+ "name": "slug",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "logo": {
+ "name": "logo",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "session_policy_settings": {
+ "name": "session_policy_settings",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "security_policy_version": {
+ "name": "security_policy_version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "whitelabel_settings": {
+ "name": "whitelabel_settings",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "data_retention_settings": {
+ "name": "data_retention_settings",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "org_usage_limit": {
+ "name": "org_usage_limit",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "storage_used_bytes": {
+ "name": "storage_used_bytes",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "limit_notifications": {
+ "name": "limit_notifications",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "departed_member_usage": {
+ "name": "departed_member_usage",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'0'"
+ },
+ "credit_balance": {
+ "name": "credit_balance",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'0'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.organization_byok_keys": {
+ "name": "organization_byok_keys",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "encrypted_api_key": {
+ "name": "encrypted_api_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "organization_byok_organization_provider_idx": {
+ "name": "organization_byok_organization_provider_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "organization_byok_keys_organization_id_organization_id_fk": {
+ "name": "organization_byok_keys_organization_id_organization_id_fk",
+ "tableFrom": "organization_byok_keys",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "organization_byok_keys_created_by_user_id_fk": {
+ "name": "organization_byok_keys_created_by_user_id_fk",
+ "tableFrom": "organization_byok_keys",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.organization_member_usage_limit": {
+ "name": "organization_member_usage_limit",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "usage_limit": {
+ "name": "usage_limit",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "set_by": {
+ "name": "set_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "org_member_usage_limit_org_user_unique": {
+ "name": "org_member_usage_limit_org_user_unique",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "org_member_usage_limit_organization_id_idx": {
+ "name": "org_member_usage_limit_organization_id_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "organization_member_usage_limit_organization_id_organization_id_fk": {
+ "name": "organization_member_usage_limit_organization_id_organization_id_fk",
+ "tableFrom": "organization_member_usage_limit",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "organization_member_usage_limit_user_id_user_id_fk": {
+ "name": "organization_member_usage_limit_user_id_user_id_fk",
+ "tableFrom": "organization_member_usage_limit",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "organization_member_usage_limit_set_by_user_id_fk": {
+ "name": "organization_member_usage_limit_set_by_user_id_fk",
+ "tableFrom": "organization_member_usage_limit",
+ "tableTo": "user",
+ "columnsFrom": ["set_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.organization_search_integration": {
+ "name": "organization_search_integration",
+ "schema": "",
+ "columns": {
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "connector_type": {
+ "name": "connector_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "approved": {
+ "name": "approved",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "organization_search_integration_organization_id_organization_id_fk": {
+ "name": "organization_search_integration_organization_id_organization_id_fk",
+ "tableFrom": "organization_search_integration",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "organization_search_integration_organization_id_connector_type_pk": {
+ "name": "organization_search_integration_organization_id_connector_type_pk",
+ "columns": ["organization_id", "connector_type"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.organization_search_invocation": {
+ "name": "organization_search_invocation",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "surface": {
+ "name": "surface",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_types": {
+ "name": "source_types",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "result_count": {
+ "name": "result_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "organization_search_invocation_org_created_idx": {
+ "name": "organization_search_invocation_org_created_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "organization_search_invocation_user_idx": {
+ "name": "organization_search_invocation_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "organization_search_invocation_organization_id_organization_id_fk": {
+ "name": "organization_search_invocation_organization_id_organization_id_fk",
+ "tableFrom": "organization_search_invocation",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "organization_search_invocation_user_id_user_id_fk": {
+ "name": "organization_search_invocation_user_id_user_id_fk",
+ "tableFrom": "organization_search_invocation",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "organization_search_invocation_result_count_bounds": {
+ "name": "organization_search_invocation_result_count_bounds",
+ "value": "\"organization_search_invocation\".\"result_count\" BETWEEN 0 AND 100"
+ },
+ "organization_search_invocation_source_types_bounds": {
+ "name": "organization_search_invocation_source_types_bounds",
+ "value": "cardinality(\"organization_search_invocation\".\"source_types\") <= 100"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.outbox_event": {
+ "name": "outbox_event",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "event_type": {
+ "name": "event_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "payload": {
+ "name": "payload",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "attempts": {
+ "name": "attempts",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "max_attempts": {
+ "name": "max_attempts",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 10
+ },
+ "available_at": {
+ "name": "available_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "locked_at": {
+ "name": "locked_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_error": {
+ "name": "last_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "processed_at": {
+ "name": "processed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "outbox_event_status_available_idx": {
+ "name": "outbox_event_status_available_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "available_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "outbox_event_pending_type_available_idx": {
+ "name": "outbox_event_pending_type_available_idx",
+ "columns": [
+ {
+ "expression": "event_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "available_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"outbox_event\".\"status\" = 'pending'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "outbox_event_locked_at_idx": {
+ "name": "outbox_event_locked_at_idx",
+ "columns": [
+ {
+ "expression": "locked_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "outbox_event_type_created_idx": {
+ "name": "outbox_event_type_created_idx",
+ "columns": [
+ {
+ "expression": "event_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.paused_executions": {
+ "name": "paused_executions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "execution_id": {
+ "name": "execution_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "execution_snapshot": {
+ "name": "execution_snapshot",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "pause_points": {
+ "name": "pause_points",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "total_pause_count": {
+ "name": "total_pause_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "resumed_count": {
+ "name": "resumed_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "automatic_resume_retry_count": {
+ "name": "automatic_resume_retry_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'paused'"
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "paused_at": {
+ "name": "paused_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "next_resume_at": {
+ "name": "next_resume_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "paused_executions_workflow_id_idx": {
+ "name": "paused_executions_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "paused_executions_status_idx": {
+ "name": "paused_executions_status_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "paused_executions_execution_id_unique": {
+ "name": "paused_executions_execution_id_unique",
+ "columns": [
+ {
+ "expression": "execution_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "paused_executions_next_resume_at_idx": {
+ "name": "paused_executions_next_resume_at_idx",
+ "columns": [
+ {
+ "expression": "next_resume_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "status = 'paused' AND next_resume_at IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "paused_executions_workflow_id_workflow_id_fk": {
+ "name": "paused_executions_workflow_id_workflow_id_fk",
+ "tableFrom": "paused_executions",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.pending_credential_draft": {
+ "name": "pending_credential_draft",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "display_name": {
+ "name": "display_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "credential_id": {
+ "name": "credential_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "oauth_config": {
+ "name": "oauth_config",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "pending_draft_organization_id_idx": {
+ "name": "pending_draft_organization_id_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "pending_draft_user_provider_org": {
+ "name": "pending_draft_user_provider_org",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "pending_draft_user_provider_ws": {
+ "name": "pending_draft_user_provider_ws",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "pending_credential_draft_user_id_user_id_fk": {
+ "name": "pending_credential_draft_user_id_user_id_fk",
+ "tableFrom": "pending_credential_draft",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "pending_credential_draft_workspace_id_workspace_id_fk": {
+ "name": "pending_credential_draft_workspace_id_workspace_id_fk",
+ "tableFrom": "pending_credential_draft",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "pending_credential_draft_organization_id_organization_id_fk": {
+ "name": "pending_credential_draft_organization_id_organization_id_fk",
+ "tableFrom": "pending_credential_draft",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "pending_credential_draft_credential_id_credential_id_fk": {
+ "name": "pending_credential_draft_credential_id_credential_id_fk",
+ "tableFrom": "pending_credential_draft",
+ "tableTo": "credential",
+ "columnsFrom": ["credential_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "pending_draft_owner_check": {
+ "name": "pending_draft_owner_check",
+ "value": "num_nonnulls(\"pending_credential_draft\".\"workspace_id\", \"pending_credential_draft\".\"organization_id\") = 1"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.permission_group": {
+ "name": "permission_group",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "config": {
+ "name": "config",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "is_default": {
+ "name": "is_default",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "membership_mode": {
+ "name": "membership_mode",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'inherit'"
+ }
+ },
+ "indexes": {
+ "permission_group_created_by_idx": {
+ "name": "permission_group_created_by_idx",
+ "columns": [
+ {
+ "expression": "created_by",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "permission_group_organization_name_unique": {
+ "name": "permission_group_organization_name_unique",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "permission_group_organization_default_unique": {
+ "name": "permission_group_organization_default_unique",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "is_default = true",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "permission_group_organization_id_organization_id_fk": {
+ "name": "permission_group_organization_id_organization_id_fk",
+ "tableFrom": "permission_group",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "permission_group_created_by_user_id_fk": {
+ "name": "permission_group_created_by_user_id_fk",
+ "tableFrom": "permission_group",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.permission_group_member": {
+ "name": "permission_group_member",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "permission_group_id": {
+ "name": "permission_group_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "assigned_by": {
+ "name": "assigned_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "assigned_at": {
+ "name": "assigned_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "permission_group_member_group_id_idx": {
+ "name": "permission_group_member_group_id_idx",
+ "columns": [
+ {
+ "expression": "permission_group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "permission_group_member_group_user_unique": {
+ "name": "permission_group_member_group_user_unique",
+ "columns": [
+ {
+ "expression": "permission_group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "permission_group_member_organization_user_idx": {
+ "name": "permission_group_member_organization_user_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "permission_group_member_permission_group_id_permission_group_id_fk": {
+ "name": "permission_group_member_permission_group_id_permission_group_id_fk",
+ "tableFrom": "permission_group_member",
+ "tableTo": "permission_group",
+ "columnsFrom": ["permission_group_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "permission_group_member_organization_id_organization_id_fk": {
+ "name": "permission_group_member_organization_id_organization_id_fk",
+ "tableFrom": "permission_group_member",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "permission_group_member_user_id_user_id_fk": {
+ "name": "permission_group_member_user_id_user_id_fk",
+ "tableFrom": "permission_group_member",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "permission_group_member_assigned_by_user_id_fk": {
+ "name": "permission_group_member_assigned_by_user_id_fk",
+ "tableFrom": "permission_group_member",
+ "tableTo": "user",
+ "columnsFrom": ["assigned_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.permission_group_workspace": {
+ "name": "permission_group_workspace",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "permission_group_id": {
+ "name": "permission_group_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "permission_group_workspace_workspace_id_idx": {
+ "name": "permission_group_workspace_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "permission_group_workspace_group_workspace_unique": {
+ "name": "permission_group_workspace_group_workspace_unique",
+ "columns": [
+ {
+ "expression": "permission_group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "permission_group_workspace_permission_group_id_permission_group_id_fk": {
+ "name": "permission_group_workspace_permission_group_id_permission_group_id_fk",
+ "tableFrom": "permission_group_workspace",
+ "tableTo": "permission_group",
+ "columnsFrom": ["permission_group_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "permission_group_workspace_workspace_id_workspace_id_fk": {
+ "name": "permission_group_workspace_workspace_id_workspace_id_fk",
+ "tableFrom": "permission_group_workspace",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "permission_group_workspace_organization_id_organization_id_fk": {
+ "name": "permission_group_workspace_organization_id_organization_id_fk",
+ "tableFrom": "permission_group_workspace",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.permissions": {
+ "name": "permissions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "entity_type": {
+ "name": "entity_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "entity_id": {
+ "name": "entity_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "permission_type": {
+ "name": "permission_type",
+ "type": "permission_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "permissions_user_id_idx": {
+ "name": "permissions_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "permissions_entity_idx": {
+ "name": "permissions_entity_idx",
+ "columns": [
+ {
+ "expression": "entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "entity_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "permissions_user_entity_type_idx": {
+ "name": "permissions_user_entity_type_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "permissions_user_entity_permission_idx": {
+ "name": "permissions_user_entity_permission_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "permission_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "permissions_unique_constraint": {
+ "name": "permissions_unique_constraint",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "entity_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "permissions_user_id_user_id_fk": {
+ "name": "permissions_user_id_user_id_fk",
+ "tableFrom": "permissions",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.pinned_item": {
+ "name": "pinned_item",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "resource_type": {
+ "name": "resource_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "resource_id": {
+ "name": "resource_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "pinned_at": {
+ "name": "pinned_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "pinned_item_user_workspace_idx": {
+ "name": "pinned_item_user_workspace_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "pinned_item_resource_idx": {
+ "name": "pinned_item_resource_idx",
+ "columns": [
+ {
+ "expression": "resource_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "resource_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "pinned_item_user_resource_unique": {
+ "name": "pinned_item_user_resource_unique",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "resource_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "resource_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "pinned_item_user_id_user_id_fk": {
+ "name": "pinned_item_user_id_user_id_fk",
+ "tableFrom": "pinned_item",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "pinned_item_workspace_id_workspace_id_fk": {
+ "name": "pinned_item_workspace_id_workspace_id_fk",
+ "tableFrom": "pinned_item",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.public_share": {
+ "name": "public_share",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "resource_type": {
+ "name": "resource_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "resource_id": {
+ "name": "resource_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_active": {
+ "name": "is_active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "auth_type": {
+ "name": "auth_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'public'"
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "allowed_emails": {
+ "name": "allowed_emails",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'[]'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "public_share_token_unique": {
+ "name": "public_share_token_unique",
+ "columns": [
+ {
+ "expression": "token",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "public_share_resource_unique": {
+ "name": "public_share_resource_unique",
+ "columns": [
+ {
+ "expression": "resource_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "resource_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "public_share_resource_id_idx": {
+ "name": "public_share_resource_id_idx",
+ "columns": [
+ {
+ "expression": "resource_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "public_share_workspace_id_idx": {
+ "name": "public_share_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "public_share_workspace_id_workspace_id_fk": {
+ "name": "public_share_workspace_id_workspace_id_fk",
+ "tableFrom": "public_share",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "public_share_created_by_user_id_fk": {
+ "name": "public_share_created_by_user_id_fk",
+ "tableFrom": "public_share",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.rate_limit_bucket": {
+ "name": "rate_limit_bucket",
+ "schema": "",
+ "columns": {
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "tokens": {
+ "name": "tokens",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "last_refill_at": {
+ "name": "last_refill_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "blocked_until": {
+ "name": "blocked_until",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "capacity_state": {
+ "name": "capacity_state",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.resource_policy": {
+ "name": "resource_policy",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "resource_type": {
+ "name": "resource_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "resource_id": {
+ "name": "resource_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "revision": {
+ "name": "revision",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "document": {
+ "name": "document",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_by": {
+ "name": "updated_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "resource_policy_organization_id_idx": {
+ "name": "resource_policy_organization_id_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "resource_policy_resource_unique": {
+ "name": "resource_policy_resource_unique",
+ "columns": [
+ {
+ "expression": "resource_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "resource_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "resource_policy_workspace_id_idx": {
+ "name": "resource_policy_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "resource_policy_workspace_id_workspace_id_fk": {
+ "name": "resource_policy_workspace_id_workspace_id_fk",
+ "tableFrom": "resource_policy",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "resource_policy_organization_id_organization_id_fk": {
+ "name": "resource_policy_organization_id_organization_id_fk",
+ "tableFrom": "resource_policy",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "resource_policy_created_by_user_id_fk": {
+ "name": "resource_policy_created_by_user_id_fk",
+ "tableFrom": "resource_policy",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "resource_policy_updated_by_user_id_fk": {
+ "name": "resource_policy_updated_by_user_id_fk",
+ "tableFrom": "resource_policy",
+ "tableTo": "user",
+ "columnsFrom": ["updated_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "resource_policy_owner_check": {
+ "name": "resource_policy_owner_check",
+ "value": "num_nonnulls(\"resource_policy\".\"workspace_id\", \"resource_policy\".\"organization_id\") = 1"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.resume_queue": {
+ "name": "resume_queue",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "paused_execution_id": {
+ "name": "paused_execution_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "parent_execution_id": {
+ "name": "parent_execution_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "new_execution_id": {
+ "name": "new_execution_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "context_id": {
+ "name": "context_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "resume_input": {
+ "name": "resume_input",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "queued_at": {
+ "name": "queued_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "claimed_at": {
+ "name": "claimed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "failure_reason": {
+ "name": "failure_reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "resume_queue_parent_status_idx": {
+ "name": "resume_queue_parent_status_idx",
+ "columns": [
+ {
+ "expression": "parent_execution_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "queued_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "resume_queue_new_execution_idx": {
+ "name": "resume_queue_new_execution_idx",
+ "columns": [
+ {
+ "expression": "new_execution_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "resume_queue_paused_execution_id_paused_executions_id_fk": {
+ "name": "resume_queue_paused_execution_id_paused_executions_id_fk",
+ "tableFrom": "resume_queue",
+ "tableTo": "paused_executions",
+ "columnsFrom": ["paused_execution_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.sandbox_image": {
+ "name": "sandbox_image",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "spec_hash": {
+ "name": "spec_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "spec": {
+ "name": "spec",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "sandbox_image_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "image_ref": {
+ "name": "image_ref",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "provider_image_id": {
+ "name": "provider_image_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "build_id": {
+ "name": "build_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "materialization_generation": {
+ "name": "materialization_generation",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error_code": {
+ "name": "error_code",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error_detail": {
+ "name": "error_detail",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_used_at": {
+ "name": "last_used_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "sandbox_image_provider_spec_unique": {
+ "name": "sandbox_image_provider_spec_unique",
+ "columns": [
+ {
+ "expression": "provider",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "spec_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "sandbox_image_status_idx": {
+ "name": "sandbox_image_status_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "sandbox_image_last_used_idx": {
+ "name": "sandbox_image_last_used_idx",
+ "columns": [
+ {
+ "expression": "last_used_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.scim_connection": {
+ "name": "scim_connection",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'active'"
+ },
+ "settings": {
+ "name": "settings",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "last_request_at": {
+ "name": "last_request_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reconcile_lock_token": {
+ "name": "reconcile_lock_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reconcile_lease_at": {
+ "name": "reconcile_lease_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reconciled_at": {
+ "name": "reconciled_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "scim_connection_organization_unique": {
+ "name": "scim_connection_organization_unique",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "scim_connection_reconcile_due_idx": {
+ "name": "scim_connection_reconcile_due_idx",
+ "columns": [
+ {
+ "expression": "reconciled_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "scim_connection_organization_id_organization_id_fk": {
+ "name": "scim_connection_organization_id_organization_id_fk",
+ "tableFrom": "scim_connection",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "scim_connection_created_by_user_id_fk": {
+ "name": "scim_connection_created_by_user_id_fk",
+ "tableFrom": "scim_connection",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.scim_credential": {
+ "name": "scim_credential",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "connection_id": {
+ "name": "connection_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token_hash": {
+ "name": "token_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token_prefix": {
+ "name": "token_prefix",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "scopes": {
+ "name": "scopes",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "revoked_at": {
+ "name": "revoked_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "revoked_by": {
+ "name": "revoked_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_used_at": {
+ "name": "last_used_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "scim_credential_token_hash_unique": {
+ "name": "scim_credential_token_hash_unique",
+ "columns": [
+ {
+ "expression": "token_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "scim_credential_connection_idx": {
+ "name": "scim_credential_connection_idx",
+ "columns": [
+ {
+ "expression": "connection_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "scim_credential_connection_id_scim_connection_id_fk": {
+ "name": "scim_credential_connection_id_scim_connection_id_fk",
+ "tableFrom": "scim_credential",
+ "tableTo": "scim_connection",
+ "columnsFrom": ["connection_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "scim_credential_revoked_by_user_id_fk": {
+ "name": "scim_credential_revoked_by_user_id_fk",
+ "tableFrom": "scim_credential",
+ "tableTo": "user",
+ "columnsFrom": ["revoked_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "scim_credential_created_by_user_id_fk": {
+ "name": "scim_credential_created_by_user_id_fk",
+ "tableFrom": "scim_credential",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.scim_group": {
+ "name": "scim_group",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "connection_id": {
+ "name": "connection_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "external_id": {
+ "name": "external_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "display_name": {
+ "name": "display_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "display_name_key": {
+ "name": "display_name_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "order_key": {
+ "name": "order_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "scim_group_connection_display_name_unique": {
+ "name": "scim_group_connection_display_name_unique",
+ "columns": [
+ {
+ "expression": "connection_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "display_name_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "scim_group_connection_external_id_unique": {
+ "name": "scim_group_connection_external_id_unique",
+ "columns": [
+ {
+ "expression": "connection_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "external_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "external_id is not null",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "scim_group_connection_order_idx": {
+ "name": "scim_group_connection_order_idx",
+ "columns": [
+ {
+ "expression": "connection_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "order_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "scim_group_connection_id_scim_connection_id_fk": {
+ "name": "scim_group_connection_id_scim_connection_id_fk",
+ "tableFrom": "scim_group",
+ "tableTo": "scim_connection",
+ "columnsFrom": ["connection_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.scim_group_mapping": {
+ "name": "scim_group_mapping",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "group_id": {
+ "name": "group_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "target_kind": {
+ "name": "target_kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "permission_group_id": {
+ "name": "permission_group_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "permission_type": {
+ "name": "permission_type",
+ "type": "permission_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source": {
+ "name": "source",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'manual'"
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "scim_group_mapping_group_idx": {
+ "name": "scim_group_mapping_group_idx",
+ "columns": [
+ {
+ "expression": "group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "scim_group_mapping_permission_group_idx": {
+ "name": "scim_group_mapping_permission_group_idx",
+ "columns": [
+ {
+ "expression": "permission_group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "scim_group_mapping_workspace_idx": {
+ "name": "scim_group_mapping_workspace_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "scim_group_mapping_group_target_unique": {
+ "name": "scim_group_mapping_group_target_unique",
+ "columns": [
+ {
+ "expression": "group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "target_kind",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "coalesce(\"permission_group_id\", \"workspace_id\", \"role\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "scim_group_mapping_group_id_scim_group_id_fk": {
+ "name": "scim_group_mapping_group_id_scim_group_id_fk",
+ "tableFrom": "scim_group_mapping",
+ "tableTo": "scim_group",
+ "columnsFrom": ["group_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "scim_group_mapping_permission_group_id_permission_group_id_fk": {
+ "name": "scim_group_mapping_permission_group_id_permission_group_id_fk",
+ "tableFrom": "scim_group_mapping",
+ "tableTo": "permission_group",
+ "columnsFrom": ["permission_group_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "scim_group_mapping_workspace_id_workspace_id_fk": {
+ "name": "scim_group_mapping_workspace_id_workspace_id_fk",
+ "tableFrom": "scim_group_mapping",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "scim_group_mapping_created_by_user_id_fk": {
+ "name": "scim_group_mapping_created_by_user_id_fk",
+ "tableFrom": "scim_group_mapping",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "scim_group_mapping_target_shape": {
+ "name": "scim_group_mapping_target_shape",
+ "value": "(\n (\"scim_group_mapping\".\"target_kind\" = 'permission_group' AND \"scim_group_mapping\".\"permission_group_id\" IS NOT NULL AND \"scim_group_mapping\".\"workspace_id\" IS NULL AND \"scim_group_mapping\".\"permission_type\" IS NULL AND \"scim_group_mapping\".\"role\" IS NULL)\n OR (\"scim_group_mapping\".\"target_kind\" = 'workspace' AND \"scim_group_mapping\".\"workspace_id\" IS NOT NULL AND \"scim_group_mapping\".\"permission_type\" IS NOT NULL AND \"scim_group_mapping\".\"permission_group_id\" IS NULL AND \"scim_group_mapping\".\"role\" IS NULL)\n OR (\"scim_group_mapping\".\"target_kind\" = 'org_role' AND \"scim_group_mapping\".\"role\" IS NOT NULL AND \"scim_group_mapping\".\"permission_group_id\" IS NULL AND \"scim_group_mapping\".\"workspace_id\" IS NULL AND \"scim_group_mapping\".\"permission_type\" IS NULL)\n )"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.scim_group_member": {
+ "name": "scim_group_member",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "group_id": {
+ "name": "group_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "scim_user_id": {
+ "name": "scim_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "scim_group_member_group_user_unique": {
+ "name": "scim_group_member_group_user_unique",
+ "columns": [
+ {
+ "expression": "group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "scim_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "scim_group_member_scim_user_idx": {
+ "name": "scim_group_member_scim_user_idx",
+ "columns": [
+ {
+ "expression": "scim_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "scim_group_member_group_id_scim_group_id_fk": {
+ "name": "scim_group_member_group_id_scim_group_id_fk",
+ "tableFrom": "scim_group_member",
+ "tableTo": "scim_group",
+ "columnsFrom": ["group_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "scim_group_member_scim_user_id_scim_user_id_fk": {
+ "name": "scim_group_member_scim_user_id_scim_user_id_fk",
+ "tableFrom": "scim_group_member",
+ "tableTo": "scim_user",
+ "columnsFrom": ["scim_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.scim_projection_grant": {
+ "name": "scim_projection_grant",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "connection_id": {
+ "name": "connection_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "scim_user_id": {
+ "name": "scim_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "target_kind": {
+ "name": "target_kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "target_id": {
+ "name": "target_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "permission_type": {
+ "name": "permission_type",
+ "type": "permission_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "baseline_permission": {
+ "name": "baseline_permission",
+ "type": "permission_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "origin": {
+ "name": "origin",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'directory'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "scim_projection_grant_user_target_unique": {
+ "name": "scim_projection_grant_user_target_unique",
+ "columns": [
+ {
+ "expression": "scim_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "target_kind",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "target_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "scim_projection_grant_connection_idx": {
+ "name": "scim_projection_grant_connection_idx",
+ "columns": [
+ {
+ "expression": "connection_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "scim_projection_grant_connection_id_scim_connection_id_fk": {
+ "name": "scim_projection_grant_connection_id_scim_connection_id_fk",
+ "tableFrom": "scim_projection_grant",
+ "tableTo": "scim_connection",
+ "columnsFrom": ["connection_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "scim_projection_grant_scim_user_id_scim_user_id_fk": {
+ "name": "scim_projection_grant_scim_user_id_scim_user_id_fk",
+ "tableFrom": "scim_projection_grant",
+ "tableTo": "scim_user",
+ "columnsFrom": ["scim_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.scim_request_log": {
+ "name": "scim_request_log",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "connection_id": {
+ "name": "connection_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "credential_id": {
+ "name": "credential_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "method": {
+ "name": "method",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "path": {
+ "name": "path",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "scim_type": {
+ "name": "scim_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "detail": {
+ "name": "detail",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_agent": {
+ "name": "user_agent",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "duration_ms": {
+ "name": "duration_ms",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "scim_request_log_connection_created_idx": {
+ "name": "scim_request_log_connection_created_idx",
+ "columns": [
+ {
+ "expression": "connection_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "scim_request_log_connection_id_scim_connection_id_fk": {
+ "name": "scim_request_log_connection_id_scim_connection_id_fk",
+ "tableFrom": "scim_request_log",
+ "tableTo": "scim_connection",
+ "columnsFrom": ["connection_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.scim_user": {
+ "name": "scim_user",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "connection_id": {
+ "name": "connection_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "external_id": {
+ "name": "external_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_name": {
+ "name": "user_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "active": {
+ "name": "active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "attributes": {
+ "name": "attributes",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "order_key": {
+ "name": "order_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "scim_user_connection_user_unique": {
+ "name": "scim_user_connection_user_unique",
+ "columns": [
+ {
+ "expression": "connection_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "scim_user_connection_user_name_unique": {
+ "name": "scim_user_connection_user_name_unique",
+ "columns": [
+ {
+ "expression": "connection_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "scim_user_connection_external_id_unique": {
+ "name": "scim_user_connection_external_id_unique",
+ "columns": [
+ {
+ "expression": "connection_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "external_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "external_id is not null",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "scim_user_connection_order_idx": {
+ "name": "scim_user_connection_order_idx",
+ "columns": [
+ {
+ "expression": "connection_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "order_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "scim_user_user_idx": {
+ "name": "scim_user_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "scim_user_connection_id_scim_connection_id_fk": {
+ "name": "scim_user_connection_id_scim_connection_id_fk",
+ "tableFrom": "scim_user",
+ "tableTo": "scim_connection",
+ "columnsFrom": ["connection_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "scim_user_user_id_user_id_fk": {
+ "name": "scim_user_user_id_user_id_fk",
+ "tableFrom": "scim_user",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.scim_user_tombstone": {
+ "name": "scim_user_tombstone",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "connection_id": {
+ "name": "connection_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "external_id": {
+ "name": "external_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "scim_user_tombstone_connection_external_id_unique": {
+ "name": "scim_user_tombstone_connection_external_id_unique",
+ "columns": [
+ {
+ "expression": "connection_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "external_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "scim_user_tombstone_user_idx": {
+ "name": "scim_user_tombstone_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "scim_user_tombstone_connection_id_scim_connection_id_fk": {
+ "name": "scim_user_tombstone_connection_id_scim_connection_id_fk",
+ "tableFrom": "scim_user_tombstone",
+ "tableTo": "scim_connection",
+ "columnsFrom": ["connection_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "scim_user_tombstone_user_id_user_id_fk": {
+ "name": "scim_user_tombstone_user_id_user_id_fk",
+ "tableFrom": "scim_user_tombstone",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.secret_usage": {
+ "name": "secret_usage",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "secret_name": {
+ "name": "secret_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "secret_scope": {
+ "name": "secret_scope",
+ "type": "secret_usage_scope",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "secret_owner_user_id": {
+ "name": "secret_owner_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "source": {
+ "name": "source",
+ "type": "secret_usage_source",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "actor_user_id": {
+ "name": "actor_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "usage_date": {
+ "name": "usage_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "use_count": {
+ "name": "use_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "last_used_at": {
+ "name": "last_used_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "last_execution_id": {
+ "name": "last_execution_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_trigger": {
+ "name": "last_trigger",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "secret_usage_bucket_unique": {
+ "name": "secret_usage_bucket_unique",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "secret_name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "secret_scope",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "secret_owner_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "source",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "actor_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "usage_date",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "secret_usage_secret_recent_idx": {
+ "name": "secret_usage_secret_recent_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "secret_name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "secret_scope",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "secret_owner_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "last_used_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "secret_usage_workspace_id_workspace_id_fk": {
+ "name": "secret_usage_workspace_id_workspace_id_fk",
+ "tableFrom": "secret_usage",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.session": {
+ "name": "session",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "ip_address": {
+ "name": "ip_address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_agent": {
+ "name": "user_agent",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "active_organization_id": {
+ "name": "active_organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "impersonated_by": {
+ "name": "impersonated_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "session_user_id_idx": {
+ "name": "session_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "session_user_id_user_id_fk": {
+ "name": "session_user_id_user_id_fk",
+ "tableFrom": "session",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "session_active_organization_id_organization_id_fk": {
+ "name": "session_active_organization_id_organization_id_fk",
+ "tableFrom": "session",
+ "tableTo": "organization",
+ "columnsFrom": ["active_organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "session_token_unique": {
+ "name": "session_token_unique",
+ "nullsNotDistinct": false,
+ "columns": ["token"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.settings": {
+ "name": "settings",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "theme": {
+ "name": "theme",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'system'"
+ },
+ "auto_connect": {
+ "name": "auto_connect",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "telemetry_enabled": {
+ "name": "telemetry_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "email_preferences": {
+ "name": "email_preferences",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "billing_usage_notifications_enabled": {
+ "name": "billing_usage_notifications_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "show_training_controls": {
+ "name": "show_training_controls",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "super_user_mode_enabled": {
+ "name": "super_user_mode_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "mothership_environment": {
+ "name": "mothership_environment",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'default'"
+ },
+ "error_notifications_enabled": {
+ "name": "error_notifications_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "snap_to_grid_size": {
+ "name": "snap_to_grid_size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "show_action_bar": {
+ "name": "show_action_bar",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "auto_focus_on_click": {
+ "name": "auto_focus_on_click",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "timezone": {
+ "name": "timezone",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "copilot_enabled_models": {
+ "name": "copilot_enabled_models",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "copilot_auto_allowed_tools": {
+ "name": "copilot_auto_allowed_tools",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'"
+ },
+ "last_active_workspace_id": {
+ "name": "last_active_workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "settings_user_id_user_id_fk": {
+ "name": "settings_user_id_user_id_fk",
+ "tableFrom": "settings",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "settings_user_id_unique": {
+ "name": "settings_user_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["user_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.sim_trigger_state": {
+ "name": "sim_trigger_state",
+ "schema": "",
+ "columns": {
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "block_id": {
+ "name": "block_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "scope_key": {
+ "name": "scope_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "last_fired_at": {
+ "name": "last_fired_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "sim_trigger_state_workflow_id_workflow_id_fk": {
+ "name": "sim_trigger_state_workflow_id_workflow_id_fk",
+ "tableFrom": "sim_trigger_state",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "sim_trigger_state_workflow_id_block_id_scope_key_pk": {
+ "name": "sim_trigger_state_workflow_id_block_id_scope_key_pk",
+ "columns": ["workflow_id", "block_id", "scope_key"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.skill": {
+ "name": "skill",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "skill_workspace_name_unique": {
+ "name": "skill_workspace_name_unique",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "skill_workspace_id_workspace_id_fk": {
+ "name": "skill_workspace_id_workspace_id_fk",
+ "tableFrom": "skill",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "skill_user_id_user_id_fk": {
+ "name": "skill_user_id_user_id_fk",
+ "tableFrom": "skill",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.skill_member": {
+ "name": "skill_member",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "skill_id": {
+ "name": "skill_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "invited_by": {
+ "name": "invited_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "skill_member_user_id_idx": {
+ "name": "skill_member_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "skill_member_unique": {
+ "name": "skill_member_unique",
+ "columns": [
+ {
+ "expression": "skill_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "skill_member_skill_id_skill_id_fk": {
+ "name": "skill_member_skill_id_skill_id_fk",
+ "tableFrom": "skill_member",
+ "tableTo": "skill",
+ "columnsFrom": ["skill_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "skill_member_user_id_user_id_fk": {
+ "name": "skill_member_user_id_user_id_fk",
+ "tableFrom": "skill_member",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "skill_member_invited_by_user_id_fk": {
+ "name": "skill_member_invited_by_user_id_fk",
+ "tableFrom": "skill_member",
+ "tableTo": "user",
+ "columnsFrom": ["invited_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.slack_app": {
+ "name": "slack_app",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "kind": {
+ "name": "kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "client_id": {
+ "name": "client_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "encrypted_client_secret": {
+ "name": "encrypted_client_secret",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "encrypted_signing_secret": {
+ "name": "encrypted_signing_secret",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "revision": {
+ "name": "revision",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "slack_app_organization_id_organization_id_fk": {
+ "name": "slack_app_organization_id_organization_id_fk",
+ "tableFrom": "slack_app",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "slack_app_owner_check": {
+ "name": "slack_app_owner_check",
+ "value": "(\"slack_app\".\"kind\" = 'custom' AND \"slack_app\".\"organization_id\" IS NOT NULL) OR (\"slack_app\".\"kind\" = 'shared' AND \"slack_app\".\"organization_id\" IS NULL)"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.slack_search_installation": {
+ "name": "slack_search_installation",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "credential_id": {
+ "name": "credential_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "app_id": {
+ "name": "app_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slack_app_id": {
+ "name": "slack_app_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "team_id": {
+ "name": "team_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "team_name": {
+ "name": "team_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "bot_user_id": {
+ "name": "bot_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "enterprise_id": {
+ "name": "enterprise_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "credential_version": {
+ "name": "credential_version",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "revision": {
+ "name": "revision",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "last_outcome": {
+ "name": "last_outcome",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_event_at": {
+ "name": "last_event_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "slack_search_installation_organization_idx": {
+ "name": "slack_search_installation_organization_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "slack_search_installation_credential_unique": {
+ "name": "slack_search_installation_credential_unique",
+ "columns": [
+ {
+ "expression": "credential_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "slack_search_installation_app_team_unique": {
+ "name": "slack_search_installation_app_team_unique",
+ "columns": [
+ {
+ "expression": "app_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "team_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "slack_search_installation_active_team_unique": {
+ "name": "slack_search_installation_active_team_unique",
+ "columns": [
+ {
+ "expression": "team_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"slack_search_installation\".\"enabled\" = true",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "slack_search_installation_organization_id_organization_id_fk": {
+ "name": "slack_search_installation_organization_id_organization_id_fk",
+ "tableFrom": "slack_search_installation",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "slack_search_installation_credential_id_credential_id_fk": {
+ "name": "slack_search_installation_credential_id_credential_id_fk",
+ "tableFrom": "slack_search_installation",
+ "tableTo": "credential",
+ "columnsFrom": ["credential_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "slack_search_installation_slack_app_id_slack_app_id_fk": {
+ "name": "slack_search_installation_slack_app_id_slack_app_id_fk",
+ "tableFrom": "slack_search_installation",
+ "tableTo": "slack_app",
+ "columnsFrom": ["slack_app_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.slack_search_turn": {
+ "name": "slack_search_turn",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "ordinal": {
+ "name": "ordinal",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "identity": {
+ "type": "always",
+ "name": "slack_search_turn_ordinal_seq",
+ "schema": "public",
+ "increment": "1",
+ "startWith": "1",
+ "minValue": "1",
+ "maxValue": "2147483647",
+ "cache": "1",
+ "cycle": false
+ }
+ },
+ "installation_id": {
+ "name": "installation_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "conversation_key": {
+ "name": "conversation_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "event_id": {
+ "name": "event_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "payload": {
+ "name": "payload",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "lease_id": {
+ "name": "lease_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "lease_expires_at": {
+ "name": "lease_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "outcome": {
+ "name": "outcome",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "slack_search_turn_event_unique": {
+ "name": "slack_search_turn_event_unique",
+ "columns": [
+ {
+ "expression": "installation_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "event_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "slack_search_turn_pending_idx": {
+ "name": "slack_search_turn_pending_idx",
+ "columns": [
+ {
+ "expression": "installation_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "slack_search_turn_thread_idx": {
+ "name": "slack_search_turn_thread_idx",
+ "columns": [
+ {
+ "expression": "conversation_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "slack_search_turn_active_thread_unique": {
+ "name": "slack_search_turn_active_thread_unique",
+ "columns": [
+ {
+ "expression": "conversation_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"slack_search_turn\".\"status\" = 'running'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "slack_search_turn_installation_id_slack_search_installation_id_fk": {
+ "name": "slack_search_turn_installation_id_slack_search_installation_id_fk",
+ "tableFrom": "slack_search_turn",
+ "tableTo": "slack_search_installation",
+ "columnsFrom": ["installation_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.sso_domain": {
+ "name": "sso_domain",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "domain": {
+ "name": "domain",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "verification_token": {
+ "name": "verification_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "verified_at": {
+ "name": "verified_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "sso_domain_organization_id_idx": {
+ "name": "sso_domain_organization_id_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "sso_domain_domain_idx": {
+ "name": "sso_domain_domain_idx",
+ "columns": [
+ {
+ "expression": "domain",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "sso_domain_org_domain_unique": {
+ "name": "sso_domain_org_domain_unique",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "domain",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "sso_domain_verified_unique": {
+ "name": "sso_domain_verified_unique",
+ "columns": [
+ {
+ "expression": "domain",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "status = 'verified'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "sso_domain_organization_id_organization_id_fk": {
+ "name": "sso_domain_organization_id_organization_id_fk",
+ "tableFrom": "sso_domain",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "sso_domain_created_by_user_id_fk": {
+ "name": "sso_domain_created_by_user_id_fk",
+ "tableFrom": "sso_domain",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.sso_provider": {
+ "name": "sso_provider",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "issuer": {
+ "name": "issuer",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "domain": {
+ "name": "domain",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "oidc_config": {
+ "name": "oidc_config",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "saml_config": {
+ "name": "saml_config",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "domain_verified": {
+ "name": "domain_verified",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "jit_provisioning_enabled": {
+ "name": "jit_provisioning_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ }
+ },
+ "indexes": {
+ "sso_provider_provider_id_unique": {
+ "name": "sso_provider_provider_id_unique",
+ "columns": [
+ {
+ "expression": "provider_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "sso_provider_org_domain_unique": {
+ "name": "sso_provider_org_domain_unique",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "lower(regexp_replace(btrim(\"domain\"), '^\\*\\.', ''))",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"sso_provider\".\"organization_id\" is not null",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "sso_provider_domain_idx": {
+ "name": "sso_provider_domain_idx",
+ "columns": [
+ {
+ "expression": "domain",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "sso_provider_user_id_idx": {
+ "name": "sso_provider_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "sso_provider_organization_id_idx": {
+ "name": "sso_provider_organization_id_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "sso_provider_user_id_user_id_fk": {
+ "name": "sso_provider_user_id_user_id_fk",
+ "tableFrom": "sso_provider",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "sso_provider_organization_id_organization_id_fk": {
+ "name": "sso_provider_organization_id_organization_id_fk",
+ "tableFrom": "sso_provider",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.subscription": {
+ "name": "subscription",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "plan": {
+ "name": "plan",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reference_id": {
+ "name": "reference_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "stripe_customer_id": {
+ "name": "stripe_customer_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "stripe_subscription_id": {
+ "name": "stripe_subscription_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "period_start": {
+ "name": "period_start",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "period_end": {
+ "name": "period_end",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cancel_at_period_end": {
+ "name": "cancel_at_period_end",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cancel_at": {
+ "name": "cancel_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "canceled_at": {
+ "name": "canceled_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ended_at": {
+ "name": "ended_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "seats": {
+ "name": "seats",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "trial_start": {
+ "name": "trial_start",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "trial_end": {
+ "name": "trial_end",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "billing_interval": {
+ "name": "billing_interval",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "stripe_schedule_id": {
+ "name": "stripe_schedule_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_closed_period_start": {
+ "name": "last_closed_period_start",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "subscription_reference_status_idx": {
+ "name": "subscription_reference_status_idx",
+ "columns": [
+ {
+ "expression": "reference_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "subscription_cycle_close_lagging_idx": {
+ "name": "subscription_cycle_close_lagging_idx",
+ "columns": [
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"subscription\".\"status\" in ('active', 'past_due') and \"subscription\".\"period_start\" is not null and (\"subscription\".\"last_closed_period_start\" is null or \"subscription\".\"last_closed_period_start\" < \"subscription\".\"period_start\")",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "check_enterprise_metadata": {
+ "name": "check_enterprise_metadata",
+ "value": "plan != 'enterprise' OR metadata IS NOT NULL"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.table_jobs": {
+ "name": "table_jobs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "table_id": {
+ "name": "table_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'running'"
+ },
+ "payload": {
+ "name": "payload",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rows_processed": {
+ "name": "rows_processed",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "error": {
+ "name": "error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "table_jobs_one_active_per_table": {
+ "name": "table_jobs_one_active_per_table",
+ "columns": [
+ {
+ "expression": "table_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"table_jobs\".\"status\" = 'running' AND \"table_jobs\".\"type\" <> 'export'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "table_jobs_watchdog_idx": {
+ "name": "table_jobs_watchdog_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "table_jobs_table_started_idx": {
+ "name": "table_jobs_table_started_idx",
+ "columns": [
+ {
+ "expression": "table_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "table_jobs_table_id_user_table_definitions_id_fk": {
+ "name": "table_jobs_table_id_user_table_definitions_id_fk",
+ "tableFrom": "table_jobs",
+ "tableTo": "user_table_definitions",
+ "columnsFrom": ["table_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "table_jobs_workspace_id_workspace_id_fk": {
+ "name": "table_jobs_workspace_id_workspace_id_fk",
+ "tableFrom": "table_jobs",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.table_row_executions": {
+ "name": "table_row_executions",
+ "schema": "",
+ "columns": {
+ "table_id": {
+ "name": "table_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "row_id": {
+ "name": "row_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "group_id": {
+ "name": "group_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "execution_id": {
+ "name": "execution_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "job_id": {
+ "name": "job_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "error": {
+ "name": "error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "running_block_ids": {
+ "name": "running_block_ids",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::text[]"
+ },
+ "block_errors": {
+ "name": "block_errors",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "cancelled_at": {
+ "name": "cancelled_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "capability_governed_user_id": {
+ "name": "capability_governed_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "enrichment_details": {
+ "name": "enrichment_details",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "table_row_executions_table_status_idx": {
+ "name": "table_row_executions_table_status_idx",
+ "columns": [
+ {
+ "expression": "table_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"table_row_executions\".\"status\" IN ('queued', 'running', 'pending')",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "table_row_executions_execution_id_idx": {
+ "name": "table_row_executions_execution_id_idx",
+ "columns": [
+ {
+ "expression": "execution_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"table_row_executions\".\"execution_id\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "table_row_executions_table_group_idx": {
+ "name": "table_row_executions_table_group_idx",
+ "columns": [
+ {
+ "expression": "table_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "table_row_executions_table_id_user_table_definitions_id_fk": {
+ "name": "table_row_executions_table_id_user_table_definitions_id_fk",
+ "tableFrom": "table_row_executions",
+ "tableTo": "user_table_definitions",
+ "columnsFrom": ["table_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "table_row_executions_row_id_user_table_rows_id_fk": {
+ "name": "table_row_executions_row_id_user_table_rows_id_fk",
+ "tableFrom": "table_row_executions",
+ "tableTo": "user_table_rows",
+ "columnsFrom": ["row_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "table_row_executions_capability_governed_user_id_user_id_fk": {
+ "name": "table_row_executions_capability_governed_user_id_user_id_fk",
+ "tableFrom": "table_row_executions",
+ "tableTo": "user",
+ "columnsFrom": ["capability_governed_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "table_row_executions_row_id_group_id_pk": {
+ "name": "table_row_executions_row_id_group_id_pk",
+ "columns": ["row_id", "group_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.table_run_dispatches": {
+ "name": "table_run_dispatches",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "table_id": {
+ "name": "table_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "request_id": {
+ "name": "request_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "mode": {
+ "name": "mode",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "scope": {
+ "name": "scope",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "cursor": {
+ "name": "cursor",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "limit": {
+ "name": "limit",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "processed_count": {
+ "name": "processed_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "is_manual_run": {
+ "name": "is_manual_run",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "triggered_by_user_id": {
+ "name": "triggered_by_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "capability_governed_user_id": {
+ "name": "capability_governed_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "requested_at": {
+ "name": "requested_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "heartbeat_at": {
+ "name": "heartbeat_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cancelled_at": {
+ "name": "cancelled_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "table_run_dispatches_active_idx": {
+ "name": "table_run_dispatches_active_idx",
+ "columns": [
+ {
+ "expression": "table_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "table_run_dispatches_watchdog_idx": {
+ "name": "table_run_dispatches_watchdog_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "requested_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "table_run_dispatches_governed_active_idx": {
+ "name": "table_run_dispatches_governed_active_idx",
+ "columns": [
+ {
+ "expression": "capability_governed_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"table_run_dispatches\".\"status\" IN ('pending', 'dispatching')",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "table_run_dispatches_table_id_user_table_definitions_id_fk": {
+ "name": "table_run_dispatches_table_id_user_table_definitions_id_fk",
+ "tableFrom": "table_run_dispatches",
+ "tableTo": "user_table_definitions",
+ "columnsFrom": ["table_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "table_run_dispatches_workspace_id_workspace_id_fk": {
+ "name": "table_run_dispatches_workspace_id_workspace_id_fk",
+ "tableFrom": "table_run_dispatches",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "table_run_dispatches_triggered_by_user_id_user_id_fk": {
+ "name": "table_run_dispatches_triggered_by_user_id_user_id_fk",
+ "tableFrom": "table_run_dispatches",
+ "tableTo": "user",
+ "columnsFrom": ["triggered_by_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "table_run_dispatches_capability_governed_user_id_user_id_fk": {
+ "name": "table_run_dispatches_capability_governed_user_id_user_id_fk",
+ "tableFrom": "table_run_dispatches",
+ "tableTo": "user",
+ "columnsFrom": ["capability_governed_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.table_views": {
+ "name": "table_views",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "table_id": {
+ "name": "table_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "config": {
+ "name": "config",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "is_default": {
+ "name": "is_default",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "table_views_table_created_idx": {
+ "name": "table_views_table_created_idx",
+ "columns": [
+ {
+ "expression": "table_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "table_views_workspace_created_idx": {
+ "name": "table_views_workspace_created_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "table_views_table_default_unique": {
+ "name": "table_views_table_default_unique",
+ "columns": [
+ {
+ "expression": "table_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "is_default = true",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "table_views_table_id_user_table_definitions_id_fk": {
+ "name": "table_views_table_id_user_table_definitions_id_fk",
+ "tableFrom": "table_views",
+ "tableTo": "user_table_definitions",
+ "columnsFrom": ["table_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "table_views_workspace_id_workspace_id_fk": {
+ "name": "table_views_workspace_id_workspace_id_fk",
+ "tableFrom": "table_views",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "table_views_created_by_user_id_fk": {
+ "name": "table_views_created_by_user_id_fk",
+ "tableFrom": "table_views",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.upload_session": {
+ "name": "upload_session",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "token_hash": {
+ "name": "token_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "knowledge_base_id": {
+ "name": "knowledge_base_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "execution_id": {
+ "name": "execution_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "purpose": {
+ "name": "purpose",
+ "type": "upload_session_purpose",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "method": {
+ "name": "method",
+ "type": "upload_session_method",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "storage_context": {
+ "name": "storage_context",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "final_key": {
+ "name": "final_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "storage_provider": {
+ "name": "storage_provider",
+ "type": "upload_session_provider",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_upload_id": {
+ "name": "provider_upload_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "provider_object_version": {
+ "name": "provider_object_version",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "file_name": {
+ "name": "file_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content_type": {
+ "name": "content_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_size": {
+ "name": "file_size",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "part_size": {
+ "name": "part_size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "part_count": {
+ "name": "part_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "upload_session_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'uploading'"
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "processing_lease_id": {
+ "name": "processing_lease_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "processing_lease_expires_at": {
+ "name": "processing_lease_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "completed_file_id": {
+ "name": "completed_file_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error": {
+ "name": "error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "upload_session_token_hash_unique": {
+ "name": "upload_session_token_hash_unique",
+ "columns": [
+ {
+ "expression": "token_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "upload_session_final_key_unique": {
+ "name": "upload_session_final_key_unique",
+ "columns": [
+ {
+ "expression": "final_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "upload_session_status_expires_at_idx": {
+ "name": "upload_session_status_expires_at_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.usage_log": {
+ "name": "usage_log",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "category": {
+ "name": "category",
+ "type": "usage_log_category",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source": {
+ "name": "source",
+ "type": "usage_log_source",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cost": {
+ "name": "cost",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "event_key": {
+ "name": "event_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "billing_entity_type": {
+ "name": "billing_entity_type",
+ "type": "billing_entity_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "billing_entity_id": {
+ "name": "billing_entity_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "billing_period_start": {
+ "name": "billing_period_start",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "billing_period_end": {
+ "name": "billing_period_end",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "execution_id": {
+ "name": "execution_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "usage_log_user_created_at_idx": {
+ "name": "usage_log_user_created_at_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "usage_log_source_idx": {
+ "name": "usage_log_source_idx",
+ "columns": [
+ {
+ "expression": "source",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "usage_log_workspace_id_idx": {
+ "name": "usage_log_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "usage_log_workflow_id_idx": {
+ "name": "usage_log_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "usage_log_event_key_unique": {
+ "name": "usage_log_event_key_unique",
+ "columns": [
+ {
+ "expression": "event_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"usage_log\".\"event_key\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "usage_log_billing_entity_period_idx": {
+ "name": "usage_log_billing_entity_period_idx",
+ "columns": [
+ {
+ "expression": "billing_entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "billing_entity_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "billing_period_start",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "billing_period_end",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "usage_log_billing_period_cost_idx": {
+ "name": "usage_log_billing_period_cost_idx",
+ "columns": [
+ {
+ "expression": "billing_entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "billing_entity_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "billing_period_start",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "billing_period_end",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "source",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "cost",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "usage_log_billing_entity_created_at_cost_idx": {
+ "name": "usage_log_billing_entity_created_at_cost_idx",
+ "columns": [
+ {
+ "expression": "billing_entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "billing_entity_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "source",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "cost",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "usage_log_workspace_created_at_idx": {
+ "name": "usage_log_workspace_created_at_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "usage_log_execution_id_idx": {
+ "name": "usage_log_execution_id_idx",
+ "columns": [
+ {
+ "expression": "execution_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "usage_log_user_id_user_id_fk": {
+ "name": "usage_log_user_id_user_id_fk",
+ "tableFrom": "usage_log",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "usage_log_workspace_id_workspace_id_fk": {
+ "name": "usage_log_workspace_id_workspace_id_fk",
+ "tableFrom": "usage_log",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "usage_log_workflow_id_workflow_id_fk": {
+ "name": "usage_log_workflow_id_workflow_id_fk",
+ "tableFrom": "usage_log",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "usage_log_billing_scope_all_or_none": {
+ "name": "usage_log_billing_scope_all_or_none",
+ "value": "(\n (\"usage_log\".\"billing_entity_type\" IS NULL AND \"usage_log\".\"billing_entity_id\" IS NULL AND \"usage_log\".\"billing_period_start\" IS NULL AND \"usage_log\".\"billing_period_end\" IS NULL)\n OR\n (\"usage_log\".\"billing_entity_type\" IS NOT NULL AND \"usage_log\".\"billing_entity_id\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" IS NOT NULL AND \"usage_log\".\"billing_period_end\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" < \"usage_log\".\"billing_period_end\")\n )"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.user": {
+ "name": "user",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "normalized_email": {
+ "name": "normalized_email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "email_verified": {
+ "name": "email_verified",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "image": {
+ "name": "image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "stripe_customer_id": {
+ "name": "stripe_customer_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'user'"
+ },
+ "banned": {
+ "name": "banned",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": false
+ },
+ "ban_reason": {
+ "name": "ban_reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ban_expires": {
+ "name": "ban_expires",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "suspended_at": {
+ "name": "suspended_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "suspension_source": {
+ "name": "suspension_source",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "user_email_lower_idx": {
+ "name": "user_email_lower_idx",
+ "columns": [
+ {
+ "expression": "lower(btrim(\"email\"))",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "user_email_unique": {
+ "name": "user_email_unique",
+ "nullsNotDistinct": false,
+ "columns": ["email"]
+ },
+ "user_normalized_email_unique": {
+ "name": "user_normalized_email_unique",
+ "nullsNotDistinct": false,
+ "columns": ["normalized_email"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user_stats": {
+ "name": "user_stats",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "total_manual_executions": {
+ "name": "total_manual_executions",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "total_api_calls": {
+ "name": "total_api_calls",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "total_webhook_triggers": {
+ "name": "total_webhook_triggers",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "total_scheduled_executions": {
+ "name": "total_scheduled_executions",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "total_chat_executions": {
+ "name": "total_chat_executions",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "total_mcp_executions": {
+ "name": "total_mcp_executions",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "total_tokens_used": {
+ "name": "total_tokens_used",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "total_cost": {
+ "name": "total_cost",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'0'"
+ },
+ "current_usage_limit": {
+ "name": "current_usage_limit",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'5'"
+ },
+ "usage_limit_updated_at": {
+ "name": "usage_limit_updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ },
+ "current_period_cost": {
+ "name": "current_period_cost",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'0'"
+ },
+ "last_period_cost": {
+ "name": "last_period_cost",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'0'"
+ },
+ "billed_overage_this_period": {
+ "name": "billed_overage_this_period",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'0'"
+ },
+ "pro_period_cost_snapshot": {
+ "name": "pro_period_cost_snapshot",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'0'"
+ },
+ "pro_period_cost_snapshot_at": {
+ "name": "pro_period_cost_snapshot_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "credit_balance": {
+ "name": "credit_balance",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'0'"
+ },
+ "total_copilot_cost": {
+ "name": "total_copilot_cost",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'0'"
+ },
+ "current_period_copilot_cost": {
+ "name": "current_period_copilot_cost",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'0'"
+ },
+ "last_period_copilot_cost": {
+ "name": "last_period_copilot_cost",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'0'"
+ },
+ "total_copilot_tokens": {
+ "name": "total_copilot_tokens",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "total_copilot_calls": {
+ "name": "total_copilot_calls",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "total_mcp_copilot_calls": {
+ "name": "total_mcp_copilot_calls",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "total_mcp_copilot_cost": {
+ "name": "total_mcp_copilot_cost",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'0'"
+ },
+ "current_period_mcp_copilot_cost": {
+ "name": "current_period_mcp_copilot_cost",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'0'"
+ },
+ "storage_used_bytes": {
+ "name": "storage_used_bytes",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "last_active": {
+ "name": "last_active",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "billing_blocked": {
+ "name": "billing_blocked",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "billing_blocked_reason": {
+ "name": "billing_blocked_reason",
+ "type": "billing_blocked_reason",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "limit_notifications": {
+ "name": "limit_notifications",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "user_stats_user_id_user_id_fk": {
+ "name": "user_stats_user_id_user_id_fk",
+ "tableFrom": "user_stats",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "user_stats_user_id_unique": {
+ "name": "user_stats_user_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["user_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user_table_definitions": {
+ "name": "user_table_definitions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "folder_id": {
+ "name": "folder_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "schema": {
+ "name": "schema",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "max_rows": {
+ "name": "max_rows",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 10000
+ },
+ "row_count": {
+ "name": "row_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "rows_version": {
+ "name": "rows_version",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "schema_locked": {
+ "name": "schema_locked",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "insert_locked": {
+ "name": "insert_locked",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "update_locked": {
+ "name": "update_locked",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "delete_locked": {
+ "name": "delete_locked",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "archived_at": {
+ "name": "archived_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "user_table_def_workspace_id_idx": {
+ "name": "user_table_def_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "user_table_def_folder_id_idx": {
+ "name": "user_table_def_folder_id_idx",
+ "columns": [
+ {
+ "expression": "folder_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "user_table_def_workspace_name_unique": {
+ "name": "user_table_def_workspace_name_unique",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"user_table_definitions\".\"archived_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "user_table_def_archived_at_idx": {
+ "name": "user_table_def_archived_at_idx",
+ "columns": [
+ {
+ "expression": "archived_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "user_table_def_workspace_archived_partial_idx": {
+ "name": "user_table_def_workspace_archived_partial_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "archived_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"user_table_definitions\".\"archived_at\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "user_table_definitions_workspace_id_workspace_id_fk": {
+ "name": "user_table_definitions_workspace_id_workspace_id_fk",
+ "tableFrom": "user_table_definitions",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "user_table_definitions_folder_id_folder_id_fk": {
+ "name": "user_table_definitions_folder_id_folder_id_fk",
+ "tableFrom": "user_table_definitions",
+ "tableTo": "folder",
+ "columnsFrom": ["folder_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "user_table_definitions_created_by_user_id_fk": {
+ "name": "user_table_definitions_created_by_user_id_fk",
+ "tableFrom": "user_table_definitions",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user_table_row_secret_provenance": {
+ "name": "user_table_row_secret_provenance",
+ "schema": "",
+ "columns": {
+ "row_id": {
+ "name": "row_id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "content_updated_at": {
+ "name": "content_updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "entries": {
+ "name": "entries",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "user_table_row_secret_provenance_row_id_user_table_rows_id_fk": {
+ "name": "user_table_row_secret_provenance_row_id_user_table_rows_id_fk",
+ "tableFrom": "user_table_row_secret_provenance",
+ "tableTo": "user_table_rows",
+ "columnsFrom": ["row_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "user_table_row_secret_provenance_status_check": {
+ "name": "user_table_row_secret_provenance_status_check",
+ "value": "\"user_table_row_secret_provenance\".\"status\" IN ('exact', 'unknown')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.user_table_rows": {
+ "name": "user_table_rows",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "table_id": {
+ "name": "table_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "data": {
+ "name": "data",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "position": {
+ "name": "position",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "order_key": {
+ "name": "order_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "secret_provenance_version": {
+ "name": "secret_provenance_version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "user_table_rows_tenant_data_gin_idx": {
+ "name": "user_table_rows_tenant_data_gin_idx",
+ "columns": [
+ {
+ "expression": "table_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "\"data\" jsonb_path_ops",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ },
+ "user_table_rows_workspace_table_idx": {
+ "name": "user_table_rows_workspace_table_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "table_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "user_table_rows_table_position_idx": {
+ "name": "user_table_rows_table_position_idx",
+ "columns": [
+ {
+ "expression": "table_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "position",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "user_table_rows_table_order_key_idx": {
+ "name": "user_table_rows_table_order_key_idx",
+ "columns": [
+ {
+ "expression": "table_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "order_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "user_table_rows_table_created_id_idx": {
+ "name": "user_table_rows_table_created_id_idx",
+ "columns": [
+ {
+ "expression": "table_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "user_table_rows_table_id_id_idx": {
+ "name": "user_table_rows_table_id_id_idx",
+ "columns": [
+ {
+ "expression": "table_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "user_table_rows_table_id_user_table_definitions_id_fk": {
+ "name": "user_table_rows_table_id_user_table_definitions_id_fk",
+ "tableFrom": "user_table_rows",
+ "tableTo": "user_table_definitions",
+ "columnsFrom": ["table_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "user_table_rows_workspace_id_workspace_id_fk": {
+ "name": "user_table_rows_workspace_id_workspace_id_fk",
+ "tableFrom": "user_table_rows",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "user_table_rows_created_by_user_id_fk": {
+ "name": "user_table_rows_created_by_user_id_fk",
+ "tableFrom": "user_table_rows",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.verification": {
+ "name": "verification",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "identifier": {
+ "name": "identifier",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "verification_identifier_idx": {
+ "name": "verification_identifier_idx",
+ "columns": [
+ {
+ "expression": "identifier",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "verification_expires_at_idx": {
+ "name": "verification_expires_at_idx",
+ "columns": [
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.waitlist": {
+ "name": "waitlist",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "waitlist_email_unique": {
+ "name": "waitlist_email_unique",
+ "nullsNotDistinct": false,
+ "columns": ["email"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.webhook": {
+ "name": "webhook",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "deployment_version_id": {
+ "name": "deployment_version_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "registration_status": {
+ "name": "registration_status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "registration_generation": {
+ "name": "registration_generation",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "config_fingerprint": {
+ "name": "config_fingerprint",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "prepared_at": {
+ "name": "prepared_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "block_id": {
+ "name": "block_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "path": {
+ "name": "path",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "routing_key": {
+ "name": "routing_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "provider_config": {
+ "name": "provider_config",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_active": {
+ "name": "is_active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "failed_count": {
+ "name": "failed_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 0
+ },
+ "last_failed_at": {
+ "name": "last_failed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "archived_at": {
+ "name": "archived_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "path_deployment_unique": {
+ "name": "path_deployment_unique",
+ "columns": [
+ {
+ "expression": "path",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "deployment_version_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"webhook\".\"archived_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "webhook_workflow_deployment_idx": {
+ "name": "webhook_workflow_deployment_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "deployment_version_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "webhook_routing_key_active_idx": {
+ "name": "webhook_routing_key_active_idx",
+ "columns": [
+ {
+ "expression": "routing_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"webhook\".\"archived_at\" IS NULL AND \"webhook\".\"routing_key\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "webhook_archived_at_partial_idx": {
+ "name": "webhook_archived_at_partial_idx",
+ "columns": [
+ {
+ "expression": "archived_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"webhook\".\"archived_at\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468": {
+ "name": "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468",
+ "columns": [
+ {
+ "expression": "provider",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "is_active",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "deployment_version_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_webhook_on_workflow_id_block_id_updated_at_desc": {
+ "name": "idx_webhook_on_workflow_id_block_id_updated_at_desc",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "block_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "webhook_active_registration_unique": {
+ "name": "webhook_active_registration_unique",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "block_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"webhook\".\"registration_status\" = 'active' AND \"webhook\".\"block_id\" IS NOT NULL AND \"webhook\".\"archived_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "webhook_candidate_registration_unique": {
+ "name": "webhook_candidate_registration_unique",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "block_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"webhook\".\"registration_status\" = 'candidate' AND \"webhook\".\"block_id\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "webhook_registration_status_generation_idx": {
+ "name": "webhook_registration_status_generation_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "registration_status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "registration_generation",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "webhook_workflow_id_workflow_id_fk": {
+ "name": "webhook_workflow_id_workflow_id_fk",
+ "tableFrom": "webhook",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "webhook_deployment_version_id_workflow_deployment_version_id_fk": {
+ "name": "webhook_deployment_version_id_workflow_deployment_version_id_fk",
+ "tableFrom": "webhook",
+ "tableTo": "workflow_deployment_version",
+ "columnsFrom": ["deployment_version_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "webhook_registration_status_check": {
+ "name": "webhook_registration_status_check",
+ "value": "\"webhook\".\"registration_status\" IS NULL OR \"webhook\".\"registration_status\" IN ('active', 'candidate', 'retired', 'orphaned')"
+ },
+ "webhook_registration_generation_check": {
+ "name": "webhook_registration_generation_check",
+ "value": "\"webhook\".\"registration_generation\" IS NULL OR \"webhook\".\"registration_generation\" >= 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.webhook_path_claim": {
+ "name": "webhook_path_claim",
+ "schema": "",
+ "columns": {
+ "path": {
+ "name": "path",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "generation": {
+ "name": "generation",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "webhook_path_claim_workflow_idx": {
+ "name": "webhook_path_claim_workflow_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "webhook_path_claim_workflow_id_workflow_id_fk": {
+ "name": "webhook_path_claim_workflow_id_workflow_id_fk",
+ "tableFrom": "webhook_path_claim",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "webhook_path_claim_generation_check": {
+ "name": "webhook_path_claim_generation_check",
+ "value": "\"webhook_path_claim\".\"generation\" >= 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.workflow": {
+ "name": "workflow",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "folder_id": {
+ "name": "folder_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sort_order": {
+ "name": "sort_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_synced": {
+ "name": "last_synced",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_deployed": {
+ "name": "is_deployed",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "deployed_at": {
+ "name": "deployed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_public_api": {
+ "name": "is_public_api",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "locked": {
+ "name": "locked",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "fork_sync_excluded": {
+ "name": "fork_sync_excluded",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "run_count": {
+ "name": "run_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "last_run_at": {
+ "name": "last_run_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "variables": {
+ "name": "variables",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'{}'"
+ },
+ "archived_at": {
+ "name": "archived_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "workflow_user_id_idx": {
+ "name": "workflow_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_workspace_id_idx": {
+ "name": "workflow_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_user_workspace_idx": {
+ "name": "workflow_user_workspace_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_workspace_folder_name_active_unique": {
+ "name": "workflow_workspace_folder_name_active_unique",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "coalesce(\"folder_id\", '')",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"workflow\".\"archived_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_folder_sort_idx": {
+ "name": "workflow_folder_sort_idx",
+ "columns": [
+ {
+ "expression": "folder_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "sort_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_active_workspace_sort_idx": {
+ "name": "workflow_active_workspace_sort_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "sort_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"workflow\".\"archived_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_archived_at_idx": {
+ "name": "workflow_archived_at_idx",
+ "columns": [
+ {
+ "expression": "archived_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_workspace_archived_partial_idx": {
+ "name": "workflow_workspace_archived_partial_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "archived_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"workflow\".\"archived_at\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workflow_user_id_user_id_fk": {
+ "name": "workflow_user_id_user_id_fk",
+ "tableFrom": "workflow",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_workspace_id_workspace_id_fk": {
+ "name": "workflow_workspace_id_workspace_id_fk",
+ "tableFrom": "workflow",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_folder_id_folder_id_fk": {
+ "name": "workflow_folder_id_folder_id_fk",
+ "tableFrom": "workflow",
+ "tableTo": "folder",
+ "columnsFrom": ["folder_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow_blocks": {
+ "name": "workflow_blocks",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "position_x": {
+ "name": "position_x",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "position_y": {
+ "name": "position_y",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "horizontal_handles": {
+ "name": "horizontal_handles",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "is_wide": {
+ "name": "is_wide",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "advanced_mode": {
+ "name": "advanced_mode",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "trigger_mode": {
+ "name": "trigger_mode",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "error_enabled": {
+ "name": "error_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "retry": {
+ "name": "retry",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "locked": {
+ "name": "locked",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "height": {
+ "name": "height",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'0'"
+ },
+ "sub_blocks": {
+ "name": "sub_blocks",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "outputs": {
+ "name": "outputs",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "data": {
+ "name": "data",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'{}'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workflow_blocks_workflow_id_idx": {
+ "name": "workflow_blocks_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_blocks_type_idx": {
+ "name": "workflow_blocks_type_idx",
+ "columns": [
+ {
+ "expression": "type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workflow_blocks_workflow_id_workflow_id_fk": {
+ "name": "workflow_blocks_workflow_id_workflow_id_fk",
+ "tableFrom": "workflow_blocks",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow_checkpoints": {
+ "name": "workflow_checkpoints",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "chat_id": {
+ "name": "chat_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "message_id": {
+ "name": "message_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "workflow_state": {
+ "name": "workflow_state",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workflow_checkpoints_user_id_idx": {
+ "name": "workflow_checkpoints_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_checkpoints_workflow_id_idx": {
+ "name": "workflow_checkpoints_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_checkpoints_chat_id_idx": {
+ "name": "workflow_checkpoints_chat_id_idx",
+ "columns": [
+ {
+ "expression": "chat_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_checkpoints_message_id_idx": {
+ "name": "workflow_checkpoints_message_id_idx",
+ "columns": [
+ {
+ "expression": "message_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_checkpoints_user_workflow_idx": {
+ "name": "workflow_checkpoints_user_workflow_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_checkpoints_workflow_chat_idx": {
+ "name": "workflow_checkpoints_workflow_chat_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "chat_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_checkpoints_created_at_idx": {
+ "name": "workflow_checkpoints_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_checkpoints_chat_created_at_idx": {
+ "name": "workflow_checkpoints_chat_created_at_idx",
+ "columns": [
+ {
+ "expression": "chat_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workflow_checkpoints_user_id_user_id_fk": {
+ "name": "workflow_checkpoints_user_id_user_id_fk",
+ "tableFrom": "workflow_checkpoints",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_checkpoints_workflow_id_workflow_id_fk": {
+ "name": "workflow_checkpoints_workflow_id_workflow_id_fk",
+ "tableFrom": "workflow_checkpoints",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_checkpoints_chat_id_copilot_chats_id_fk": {
+ "name": "workflow_checkpoints_chat_id_copilot_chats_id_fk",
+ "tableFrom": "workflow_checkpoints",
+ "tableTo": "copilot_chats",
+ "columnsFrom": ["chat_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow_deployment_operation": {
+ "name": "workflow_deployment_operation",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "deployment_version_id": {
+ "name": "deployment_version_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "version": {
+ "name": "version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "previous_active_version_id": {
+ "name": "previous_active_version_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "action": {
+ "name": "action",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "protocol_version": {
+ "name": "protocol_version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "generation": {
+ "name": "generation",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'preparing'"
+ },
+ "component_readiness": {
+ "name": "component_readiness",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "error_code": {
+ "name": "error_code",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "idempotency_key": {
+ "name": "idempotency_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "request_hash": {
+ "name": "request_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "actor_id": {
+ "name": "actor_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workflow_deployment_operation_workflow_generation_unique": {
+ "name": "workflow_deployment_operation_workflow_generation_unique",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "generation",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_deployment_operation_workflow_idempotency_unique": {
+ "name": "workflow_deployment_operation_workflow_idempotency_unique",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "idempotency_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"workflow_deployment_operation\".\"idempotency_key\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_deployment_operation_workflow_in_flight_unique": {
+ "name": "workflow_deployment_operation_workflow_in_flight_unique",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"workflow_deployment_operation\".\"status\" IN ('preparing', 'activating')",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_deployment_operation_workflow_status_idx": {
+ "name": "workflow_deployment_operation_workflow_status_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_deployment_operation_deployment_version_idx": {
+ "name": "workflow_deployment_operation_deployment_version_idx",
+ "columns": [
+ {
+ "expression": "deployment_version_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_deployment_operation_workflow_version_generation_idx": {
+ "name": "workflow_deployment_operation_workflow_version_generation_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "deployment_version_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "generation",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workflow_deployment_operation_workflow_id_workflow_id_fk": {
+ "name": "workflow_deployment_operation_workflow_id_workflow_id_fk",
+ "tableFrom": "workflow_deployment_operation",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_deployment_operation_deployment_version_id_workflow_deployment_version_id_fk": {
+ "name": "workflow_deployment_operation_deployment_version_id_workflow_deployment_version_id_fk",
+ "tableFrom": "workflow_deployment_operation",
+ "tableTo": "workflow_deployment_version",
+ "columnsFrom": ["deployment_version_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_deployment_operation_previous_active_version_id_workflow_deployment_version_id_fk": {
+ "name": "workflow_deployment_operation_previous_active_version_id_workflow_deployment_version_id_fk",
+ "tableFrom": "workflow_deployment_operation",
+ "tableTo": "workflow_deployment_version",
+ "columnsFrom": ["previous_active_version_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "workflow_deployment_operation_action_check": {
+ "name": "workflow_deployment_operation_action_check",
+ "value": "\"workflow_deployment_operation\".\"action\" IN ('deploy', 'activate')"
+ },
+ "workflow_deployment_operation_status_check": {
+ "name": "workflow_deployment_operation_status_check",
+ "value": "\"workflow_deployment_operation\".\"status\" IN ('preparing', 'activating', 'active', 'failed', 'superseded')"
+ },
+ "workflow_deployment_operation_generation_check": {
+ "name": "workflow_deployment_operation_generation_check",
+ "value": "\"workflow_deployment_operation\".\"generation\" > 0"
+ },
+ "workflow_deployment_operation_protocol_version_check": {
+ "name": "workflow_deployment_operation_protocol_version_check",
+ "value": "\"workflow_deployment_operation\".\"protocol_version\" > 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.workflow_deployment_version": {
+ "name": "workflow_deployment_version",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "version": {
+ "name": "version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "state": {
+ "name": "state",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_active": {
+ "name": "is_active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "workflow_deployment_version_workflow_version_unique": {
+ "name": "workflow_deployment_version_workflow_version_unique",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "version",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_deployment_version_workflow_active_idx": {
+ "name": "workflow_deployment_version_workflow_active_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "is_active",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_deployment_version_created_at_idx": {
+ "name": "workflow_deployment_version_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workflow_deployment_version_workflow_id_workflow_id_fk": {
+ "name": "workflow_deployment_version_workflow_id_workflow_id_fk",
+ "tableFrom": "workflow_deployment_version",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow_edges": {
+ "name": "workflow_edges",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_block_id": {
+ "name": "source_block_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "target_block_id": {
+ "name": "target_block_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_handle": {
+ "name": "source_handle",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "target_handle": {
+ "name": "target_handle",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workflow_edges_workflow_id_idx": {
+ "name": "workflow_edges_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_edges_workflow_source_idx": {
+ "name": "workflow_edges_workflow_source_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "source_block_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_edges_workflow_target_idx": {
+ "name": "workflow_edges_workflow_target_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "target_block_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workflow_edges_workflow_id_workflow_id_fk": {
+ "name": "workflow_edges_workflow_id_workflow_id_fk",
+ "tableFrom": "workflow_edges",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_edges_source_block_id_workflow_blocks_id_fk": {
+ "name": "workflow_edges_source_block_id_workflow_blocks_id_fk",
+ "tableFrom": "workflow_edges",
+ "tableTo": "workflow_blocks",
+ "columnsFrom": ["source_block_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_edges_target_block_id_workflow_blocks_id_fk": {
+ "name": "workflow_edges_target_block_id_workflow_blocks_id_fk",
+ "tableFrom": "workflow_edges",
+ "tableTo": "workflow_blocks",
+ "columnsFrom": ["target_block_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow_execution_logs": {
+ "name": "workflow_execution_logs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "execution_id": {
+ "name": "execution_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "state_snapshot_id": {
+ "name": "state_snapshot_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "deployment_version_id": {
+ "name": "deployment_version_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "level": {
+ "name": "level",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'running'"
+ },
+ "trigger": {
+ "name": "trigger",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "execution_deadline_at": {
+ "name": "execution_deadline_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ended_at": {
+ "name": "ended_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "total_duration_ms": {
+ "name": "total_duration_ms",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "execution_data": {
+ "name": "execution_data",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "cost": {
+ "name": "cost",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cost_total": {
+ "name": "cost_total",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "models_used": {
+ "name": "models_used",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "files": {
+ "name": "files",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workflow_execution_logs_workflow_id_idx": {
+ "name": "workflow_execution_logs_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_execution_logs_state_snapshot_id_idx": {
+ "name": "workflow_execution_logs_state_snapshot_id_idx",
+ "columns": [
+ {
+ "expression": "state_snapshot_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_execution_logs_deployment_version_id_idx": {
+ "name": "workflow_execution_logs_deployment_version_id_idx",
+ "columns": [
+ {
+ "expression": "deployment_version_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_execution_logs_trigger_idx": {
+ "name": "workflow_execution_logs_trigger_idx",
+ "columns": [
+ {
+ "expression": "trigger",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_execution_logs_level_idx": {
+ "name": "workflow_execution_logs_level_idx",
+ "columns": [
+ {
+ "expression": "level",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_execution_logs_started_at_idx": {
+ "name": "workflow_execution_logs_started_at_idx",
+ "columns": [
+ {
+ "expression": "started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_execution_logs_execution_id_unique": {
+ "name": "workflow_execution_logs_execution_id_unique",
+ "columns": [
+ {
+ "expression": "execution_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_execution_logs_workflow_started_at_idx": {
+ "name": "workflow_execution_logs_workflow_started_at_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_execution_logs_workspace_started_at_idx": {
+ "name": "workflow_execution_logs_workspace_started_at_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_execution_logs_workspace_started_at_id_desc_idx": {
+ "name": "workflow_execution_logs_workspace_started_at_id_desc_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "\"started_at\" DESC NULLS LAST",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "\"id\" DESC",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_execution_logs_workspace_cost_total_idx": {
+ "name": "workflow_execution_logs_workspace_cost_total_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "cost_total",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_execution_logs_models_used_idx": {
+ "name": "workflow_execution_logs_models_used_idx",
+ "columns": [
+ {
+ "expression": "models_used",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ },
+ "workflow_execution_logs_workspace_ended_at_id_idx": {
+ "name": "workflow_execution_logs_workspace_ended_at_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "date_trunc('milliseconds', \"ended_at\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_execution_logs_running_started_at_idx": {
+ "name": "workflow_execution_logs_running_started_at_idx",
+ "columns": [
+ {
+ "expression": "started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "status = 'running'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_execution_logs_running_deadline_idx": {
+ "name": "workflow_execution_logs_running_deadline_idx",
+ "columns": [
+ {
+ "expression": "execution_deadline_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"workflow_execution_logs\".\"status\" = 'running' AND \"workflow_execution_logs\".\"execution_deadline_at\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_execution_logs_redacting_started_at_idx": {
+ "name": "workflow_execution_logs_redacting_started_at_idx",
+ "columns": [
+ {
+ "expression": "started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "status = 'redacting'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_execution_logs_redacting_deadline_idx": {
+ "name": "workflow_execution_logs_redacting_deadline_idx",
+ "columns": [
+ {
+ "expression": "execution_deadline_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"workflow_execution_logs\".\"status\" = 'redacting' AND \"workflow_execution_logs\".\"execution_deadline_at\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_execution_logs_completed_ended_at_idx": {
+ "name": "workflow_execution_logs_completed_ended_at_idx",
+ "columns": [
+ {
+ "expression": "ended_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "execution_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"workflow_execution_logs\".\"status\" = 'completed' AND \"workflow_execution_logs\".\"level\" = 'info' AND \"workflow_execution_logs\".\"ended_at\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workflow_execution_logs_workflow_id_workflow_id_fk": {
+ "name": "workflow_execution_logs_workflow_id_workflow_id_fk",
+ "tableFrom": "workflow_execution_logs",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "workflow_execution_logs_workspace_id_workspace_id_fk": {
+ "name": "workflow_execution_logs_workspace_id_workspace_id_fk",
+ "tableFrom": "workflow_execution_logs",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk": {
+ "name": "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk",
+ "tableFrom": "workflow_execution_logs",
+ "tableTo": "workflow_execution_snapshots",
+ "columnsFrom": ["state_snapshot_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk": {
+ "name": "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk",
+ "tableFrom": "workflow_execution_logs",
+ "tableTo": "workflow_deployment_version",
+ "columnsFrom": ["deployment_version_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow_execution_snapshots": {
+ "name": "workflow_execution_snapshots",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "state_hash": {
+ "name": "state_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "state_data": {
+ "name": "state_data",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workflow_snapshots_workflow_id_idx": {
+ "name": "workflow_snapshots_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_snapshots_hash_idx": {
+ "name": "workflow_snapshots_hash_idx",
+ "columns": [
+ {
+ "expression": "state_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_snapshots_workflow_hash_idx": {
+ "name": "workflow_snapshots_workflow_hash_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "state_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_snapshots_created_at_idx": {
+ "name": "workflow_snapshots_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workflow_execution_snapshots_workflow_id_workflow_id_fk": {
+ "name": "workflow_execution_snapshots_workflow_id_workflow_id_fk",
+ "tableFrom": "workflow_execution_snapshots",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow_mcp_server": {
+ "name": "workflow_mcp_server",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_public": {
+ "name": "is_public",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workflow_mcp_server_workspace_id_idx": {
+ "name": "workflow_mcp_server_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_mcp_server_created_by_idx": {
+ "name": "workflow_mcp_server_created_by_idx",
+ "columns": [
+ {
+ "expression": "created_by",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_mcp_server_deleted_at_idx": {
+ "name": "workflow_mcp_server_deleted_at_idx",
+ "columns": [
+ {
+ "expression": "deleted_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_mcp_server_workspace_deleted_partial_idx": {
+ "name": "workflow_mcp_server_workspace_deleted_partial_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "deleted_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"workflow_mcp_server\".\"deleted_at\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workflow_mcp_server_workspace_id_workspace_id_fk": {
+ "name": "workflow_mcp_server_workspace_id_workspace_id_fk",
+ "tableFrom": "workflow_mcp_server",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_mcp_server_created_by_user_id_fk": {
+ "name": "workflow_mcp_server_created_by_user_id_fk",
+ "tableFrom": "workflow_mcp_server",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow_mcp_tool": {
+ "name": "workflow_mcp_tool",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "server_id": {
+ "name": "server_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tool_name": {
+ "name": "tool_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tool_description": {
+ "name": "tool_description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "parameter_schema": {
+ "name": "parameter_schema",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "parameter_description_overrides": {
+ "name": "parameter_description_overrides",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::json"
+ },
+ "archived_at": {
+ "name": "archived_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workflow_mcp_tool_server_id_idx": {
+ "name": "workflow_mcp_tool_server_id_idx",
+ "columns": [
+ {
+ "expression": "server_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_mcp_tool_workflow_id_idx": {
+ "name": "workflow_mcp_tool_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_mcp_tool_server_workflow_unique": {
+ "name": "workflow_mcp_tool_server_workflow_unique",
+ "columns": [
+ {
+ "expression": "server_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"workflow_mcp_tool\".\"archived_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_mcp_tool_archived_at_partial_idx": {
+ "name": "workflow_mcp_tool_archived_at_partial_idx",
+ "columns": [
+ {
+ "expression": "archived_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"workflow_mcp_tool\".\"archived_at\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk": {
+ "name": "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk",
+ "tableFrom": "workflow_mcp_tool",
+ "tableTo": "workflow_mcp_server",
+ "columnsFrom": ["server_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_mcp_tool_workflow_id_workflow_id_fk": {
+ "name": "workflow_mcp_tool_workflow_id_workflow_id_fk",
+ "tableFrom": "workflow_mcp_tool",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow_schedule": {
+ "name": "workflow_schedule",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "deployment_version_id": {
+ "name": "deployment_version_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "deployment_operation_id": {
+ "name": "deployment_operation_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "block_id": {
+ "name": "block_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cron_expression": {
+ "name": "cron_expression",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "next_run_at": {
+ "name": "next_run_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_ran_at": {
+ "name": "last_ran_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_queued_at": {
+ "name": "last_queued_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "trigger_type": {
+ "name": "trigger_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "timezone": {
+ "name": "timezone",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'UTC'"
+ },
+ "failed_count": {
+ "name": "failed_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "infra_retry_count": {
+ "name": "infra_retry_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'active'"
+ },
+ "last_failed_at": {
+ "name": "last_failed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source_type": {
+ "name": "source_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'workflow'"
+ },
+ "job_title": {
+ "name": "job_title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "prompt": {
+ "name": "prompt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "lifecycle": {
+ "name": "lifecycle",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'persistent'"
+ },
+ "success_condition": {
+ "name": "success_condition",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "max_runs": {
+ "name": "max_runs",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "run_count": {
+ "name": "run_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "source_chat_id": {
+ "name": "source_chat_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source_task_name": {
+ "name": "source_task_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source_user_id": {
+ "name": "source_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source_workspace_id": {
+ "name": "source_workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "secret_scope": {
+ "name": "secret_scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'all'"
+ },
+ "mounted_secrets": {
+ "name": "mounted_secrets",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "job_history": {
+ "name": "job_history",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "contexts": {
+ "name": "contexts",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "excluded_dates": {
+ "name": "excluded_dates",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ends_at": {
+ "name": "ends_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "archived_at": {
+ "name": "archived_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workflow_schedule_workflow_block_deployment_unique": {
+ "name": "workflow_schedule_workflow_block_deployment_unique",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "block_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "deployment_version_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"workflow_schedule\".\"archived_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_schedule_workflow_deployment_idx": {
+ "name": "workflow_schedule_workflow_deployment_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "deployment_version_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_schedule_archived_at_partial_idx": {
+ "name": "workflow_schedule_archived_at_partial_idx",
+ "columns": [
+ {
+ "expression": "archived_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"workflow_schedule\".\"archived_at\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6": {
+ "name": "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6",
+ "columns": [
+ {
+ "expression": "source_workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "source_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "archived_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_schedule_due_workflow_idx": {
+ "name": "workflow_schedule_due_workflow_idx",
+ "columns": [
+ {
+ "expression": "next_run_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "last_queued_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "deployment_version_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND (\"workflow_schedule\".\"source_type\" = 'workflow' OR \"workflow_schedule\".\"source_type\" IS NULL)",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_schedule_due_job_idx": {
+ "name": "workflow_schedule_due_job_idx",
+ "columns": [
+ {
+ "expression": "next_run_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "last_queued_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND \"workflow_schedule\".\"source_type\" = 'job'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workflow_schedule_workflow_id_workflow_id_fk": {
+ "name": "workflow_schedule_workflow_id_workflow_id_fk",
+ "tableFrom": "workflow_schedule",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk": {
+ "name": "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk",
+ "tableFrom": "workflow_schedule",
+ "tableTo": "workflow_deployment_version",
+ "columnsFrom": ["deployment_version_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_schedule_deployment_operation_id_workflow_deployment_operation_id_fk": {
+ "name": "workflow_schedule_deployment_operation_id_workflow_deployment_operation_id_fk",
+ "tableFrom": "workflow_schedule",
+ "tableTo": "workflow_deployment_operation",
+ "columnsFrom": ["deployment_operation_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "workflow_schedule_source_user_id_user_id_fk": {
+ "name": "workflow_schedule_source_user_id_user_id_fk",
+ "tableFrom": "workflow_schedule",
+ "tableTo": "user",
+ "columnsFrom": ["source_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_schedule_source_workspace_id_workspace_id_fk": {
+ "name": "workflow_schedule_source_workspace_id_workspace_id_fk",
+ "tableFrom": "workflow_schedule",
+ "tableTo": "workspace",
+ "columnsFrom": ["source_workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow_subflows": {
+ "name": "workflow_subflows",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "config": {
+ "name": "config",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workflow_subflows_workflow_id_idx": {
+ "name": "workflow_subflows_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_subflows_workflow_type_idx": {
+ "name": "workflow_subflows_workflow_type_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workflow_subflows_workflow_id_workflow_id_fk": {
+ "name": "workflow_subflows_workflow_id_workflow_id_fk",
+ "tableFrom": "workflow_subflows",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workspace": {
+ "name": "workspace",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "color": {
+ "name": "color",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'#33C482'"
+ },
+ "logo_url": {
+ "name": "logo_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "owner_id": {
+ "name": "owner_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "workspace_mode": {
+ "name": "workspace_mode",
+ "type": "workspace_mode",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'grandfathered_shared'"
+ },
+ "billed_account_user_id": {
+ "name": "billed_account_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "storage_used_bytes": {
+ "name": "storage_used_bytes",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "allow_personal_api_keys": {
+ "name": "allow_personal_api_keys",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "inbox_enabled": {
+ "name": "inbox_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "inbox_address": {
+ "name": "inbox_address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "inbox_provider_id": {
+ "name": "inbox_provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "inbox_secret_scope": {
+ "name": "inbox_secret_scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'all'"
+ },
+ "inbox_mounted_secrets": {
+ "name": "inbox_mounted_secrets",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "archived_at": {
+ "name": "archived_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "organization_assigned_at": {
+ "name": "organization_assigned_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "forked_from_workspace_id": {
+ "name": "forked_from_workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workspace_owner_id_idx": {
+ "name": "workspace_owner_id_idx",
+ "columns": [
+ {
+ "expression": "owner_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_organization_id_idx": {
+ "name": "workspace_organization_id_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_mode_idx": {
+ "name": "workspace_mode_idx",
+ "columns": [
+ {
+ "expression": "workspace_mode",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_forked_from_workspace_id_idx": {
+ "name": "workspace_forked_from_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "forked_from_workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_inbox_provider_id_idx": {
+ "name": "workspace_inbox_provider_id_idx",
+ "columns": [
+ {
+ "expression": "inbox_provider_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"workspace\".\"inbox_provider_id\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workspace_owner_id_user_id_fk": {
+ "name": "workspace_owner_id_user_id_fk",
+ "tableFrom": "workspace",
+ "tableTo": "user",
+ "columnsFrom": ["owner_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workspace_organization_id_organization_id_fk": {
+ "name": "workspace_organization_id_organization_id_fk",
+ "tableFrom": "workspace",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "workspace_billed_account_user_id_user_id_fk": {
+ "name": "workspace_billed_account_user_id_user_id_fk",
+ "tableFrom": "workspace",
+ "tableTo": "user",
+ "columnsFrom": ["billed_account_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "workspace_forked_from_workspace_id_workspace_id_fk": {
+ "name": "workspace_forked_from_workspace_id_workspace_id_fk",
+ "tableFrom": "workspace",
+ "tableTo": "workspace",
+ "columnsFrom": ["forked_from_workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "workspace_storage_used_bytes_non_negative": {
+ "name": "workspace_storage_used_bytes_non_negative",
+ "value": "\"workspace\".\"storage_used_bytes\" >= 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.workspace_byok_keys": {
+ "name": "workspace_byok_keys",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "encrypted_api_key": {
+ "name": "encrypted_api_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workspace_byok_workspace_provider_idx": {
+ "name": "workspace_byok_workspace_provider_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workspace_byok_keys_workspace_id_workspace_id_fk": {
+ "name": "workspace_byok_keys_workspace_id_workspace_id_fk",
+ "tableFrom": "workspace_byok_keys",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workspace_byok_keys_created_by_user_id_fk": {
+ "name": "workspace_byok_keys_created_by_user_id_fk",
+ "tableFrom": "workspace_byok_keys",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workspace_environment": {
+ "name": "workspace_environment",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "variables": {
+ "name": "variables",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workspace_environment_workspace_unique": {
+ "name": "workspace_environment_workspace_unique",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workspace_environment_workspace_id_workspace_id_fk": {
+ "name": "workspace_environment_workspace_id_workspace_id_fk",
+ "tableFrom": "workspace_environment",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workspace_file": {
+ "name": "workspace_file",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "size": {
+ "name": "size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "uploaded_by": {
+ "name": "uploaded_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "uploaded_at": {
+ "name": "uploaded_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workspace_file_workspace_id_idx": {
+ "name": "workspace_file_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_file_deleted_at_idx": {
+ "name": "workspace_file_deleted_at_idx",
+ "columns": [
+ {
+ "expression": "deleted_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_file_workspace_deleted_partial_idx": {
+ "name": "workspace_file_workspace_deleted_partial_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "deleted_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"workspace_file\".\"deleted_at\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workspace_file_workspace_id_workspace_id_fk": {
+ "name": "workspace_file_workspace_id_workspace_id_fk",
+ "tableFrom": "workspace_file",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workspace_file_uploaded_by_user_id_fk": {
+ "name": "workspace_file_uploaded_by_user_id_fk",
+ "tableFrom": "workspace_file",
+ "tableTo": "user",
+ "columnsFrom": ["uploaded_by"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "workspace_file_key_unique": {
+ "name": "workspace_file_key_unique",
+ "nullsNotDistinct": false,
+ "columns": ["key"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workspace_file_collab_state": {
+ "name": "workspace_file_collab_state",
+ "schema": "",
+ "columns": {
+ "file_id": {
+ "name": "file_id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "doc_state": {
+ "name": "doc_state",
+ "type": "bytea",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_hash": {
+ "name": "source_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "workspace_file_collab_state_file_id_workspace_files_id_fk": {
+ "name": "workspace_file_collab_state_file_id_workspace_files_id_fk",
+ "tableFrom": "workspace_file_collab_state",
+ "tableTo": "workspace_files",
+ "columnsFrom": ["file_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workspace_file_search_backfill": {
+ "name": "workspace_file_search_backfill",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "after_workspace_id": {
+ "name": "after_workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "after_file_id": {
+ "name": "after_file_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workspace_file_search_dispatch_queue": {
+ "name": "workspace_file_search_dispatch_queue",
+ "schema": "",
+ "columns": {
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "enqueued_at": {
+ "name": "enqueued_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "last_dispatched_at": {
+ "name": "last_dispatched_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workspace_file_search_dispatch_queue_schedule_idx": {
+ "name": "workspace_file_search_dispatch_queue_schedule_idx",
+ "columns": [
+ {
+ "expression": "last_dispatched_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "first"
+ },
+ {
+ "expression": "enqueued_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workspace_file_search_queue_workspace_fk": {
+ "name": "workspace_file_search_queue_workspace_fk",
+ "tableFrom": "workspace_file_search_dispatch_queue",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workspace_file_search_index": {
+ "name": "workspace_file_search_index",
+ "schema": "",
+ "columns": {
+ "file_id": {
+ "name": "file_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_content_updated_at": {
+ "name": "source_content_updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "workspace_file_search_index_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "partial": {
+ "name": "partial",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "failure_reason": {
+ "name": "failure_reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "line_count": {
+ "name": "line_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "indexed_bytes": {
+ "name": "indexed_bytes",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "dispatched_at": {
+ "name": "dispatched_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workspace_file_search_index_workspace_status_idx": {
+ "name": "workspace_file_search_index_workspace_status_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "source_content_updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_file_search_index_pending_dispatch_idx": {
+ "name": "workspace_file_search_index_pending_dispatch_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "file_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "source_content_updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"workspace_file_search_index\".\"status\" = 'pending' AND \"workspace_file_search_index\".\"dispatched_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_file_search_index_active_dispatch_idx": {
+ "name": "workspace_file_search_index_active_dispatch_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "dispatched_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"workspace_file_search_index\".\"status\" = 'pending' AND \"workspace_file_search_index\".\"dispatched_at\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workspace_file_search_index_file_fk": {
+ "name": "workspace_file_search_index_file_fk",
+ "tableFrom": "workspace_file_search_index",
+ "tableTo": "workspace_files",
+ "columnsFrom": ["file_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workspace_file_search_index_workspace_fk": {
+ "name": "workspace_file_search_index_workspace_fk",
+ "tableFrom": "workspace_file_search_index",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "workspace_file_search_index_pk": {
+ "name": "workspace_file_search_index_pk",
+ "columns": ["file_id", "source_content_updated_at"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workspace_file_search_segment": {
+ "name": "workspace_file_search_segment",
+ "schema": "",
+ "columns": {
+ "file_id": {
+ "name": "file_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_content_updated_at": {
+ "name": "source_content_updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "line_number": {
+ "name": "line_number",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "segment_number": {
+ "name": "segment_number",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "segment_start": {
+ "name": "segment_start",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "line_length": {
+ "name": "line_length",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "workspace_file_search_segment_workspace_revision_idx": {
+ "name": "workspace_file_search_segment_workspace_revision_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "file_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "source_content_updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_file_search_segment_workspace_content_trgm_idx": {
+ "name": "workspace_file_search_segment_workspace_content_trgm_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last",
+ "opclass": "text_ops"
+ },
+ {
+ "expression": "content",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last",
+ "opclass": "gin_trgm_ops"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workspace_file_search_segment_file_fk": {
+ "name": "workspace_file_search_segment_file_fk",
+ "tableFrom": "workspace_file_search_segment",
+ "tableTo": "workspace_files",
+ "columnsFrom": ["file_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workspace_file_search_segment_workspace_fk": {
+ "name": "workspace_file_search_segment_workspace_fk",
+ "tableFrom": "workspace_file_search_segment",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "workspace_file_search_segment_pk": {
+ "name": "workspace_file_search_segment_pk",
+ "columns": ["file_id", "source_content_updated_at", "line_number", "segment_number"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workspace_file_secret_provenance": {
+ "name": "workspace_file_secret_provenance",
+ "schema": "",
+ "columns": {
+ "file_id": {
+ "name": "file_id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "content_updated_at": {
+ "name": "content_updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "entries": {
+ "name": "entries",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "workspace_file_secret_provenance_file_id_workspace_files_id_fk": {
+ "name": "workspace_file_secret_provenance_file_id_workspace_files_id_fk",
+ "tableFrom": "workspace_file_secret_provenance",
+ "tableTo": "workspace_files",
+ "columnsFrom": ["file_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "workspace_file_secret_provenance_status_check": {
+ "name": "workspace_file_secret_provenance_status_check",
+ "value": "\"workspace_file_secret_provenance\".\"status\" IN ('exact', 'unknown', 'unrecorded')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.workspace_files": {
+ "name": "workspace_files",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "folder_id": {
+ "name": "folder_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "context": {
+ "name": "context",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "chat_id": {
+ "name": "chat_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "message_id": {
+ "name": "message_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "original_name": {
+ "name": "original_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "display_name": {
+ "name": "display_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "content_type": {
+ "name": "content_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "size": {
+ "name": "size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "size_bytes": {
+ "name": "size_bytes",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "width": {
+ "name": "width",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "height": {
+ "name": "height",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "uploaded_at": {
+ "name": "uploaded_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "content_updated_at": {
+ "name": "content_updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "secret_provenance_version": {
+ "name": "secret_provenance_version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "workspace_files_key_active_unique": {
+ "name": "workspace_files_key_active_unique",
+ "columns": [
+ {
+ "expression": "key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"workspace_files\".\"deleted_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_files_workspace_folder_name_active_unique": {
+ "name": "workspace_files_workspace_folder_name_active_unique",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "coalesce(\"folder_id\", '')",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "original_name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"workspace_files\".\"deleted_at\" IS NULL AND \"workspace_files\".\"context\" = 'workspace' AND \"workspace_files\".\"workspace_id\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_files_chat_display_name_unique": {
+ "name": "workspace_files_chat_display_name_unique",
+ "columns": [
+ {
+ "expression": "chat_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "display_name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"workspace_files\".\"context\" = 'mothership' AND \"workspace_files\".\"chat_id\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_files_organization_id_idx": {
+ "name": "workspace_files_organization_id_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_files_key_idx": {
+ "name": "workspace_files_key_idx",
+ "columns": [
+ {
+ "expression": "key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_files_user_id_idx": {
+ "name": "workspace_files_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_files_workspace_id_idx": {
+ "name": "workspace_files_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_files_folder_id_idx": {
+ "name": "workspace_files_folder_id_idx",
+ "columns": [
+ {
+ "expression": "folder_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_files_context_idx": {
+ "name": "workspace_files_context_idx",
+ "columns": [
+ {
+ "expression": "context",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_files_chat_id_idx": {
+ "name": "workspace_files_chat_id_idx",
+ "columns": [
+ {
+ "expression": "chat_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_files_deleted_at_idx": {
+ "name": "workspace_files_deleted_at_idx",
+ "columns": [
+ {
+ "expression": "deleted_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_files_workspace_deleted_partial_idx": {
+ "name": "workspace_files_workspace_deleted_partial_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "deleted_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"workspace_files\".\"deleted_at\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workspace_files_user_id_user_id_fk": {
+ "name": "workspace_files_user_id_user_id_fk",
+ "tableFrom": "workspace_files",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workspace_files_workspace_id_workspace_id_fk": {
+ "name": "workspace_files_workspace_id_workspace_id_fk",
+ "tableFrom": "workspace_files",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workspace_files_organization_id_organization_id_fk": {
+ "name": "workspace_files_organization_id_organization_id_fk",
+ "tableFrom": "workspace_files",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workspace_files_folder_id_folder_id_fk": {
+ "name": "workspace_files_folder_id_folder_id_fk",
+ "tableFrom": "workspace_files",
+ "tableTo": "folder",
+ "columnsFrom": ["folder_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "workspace_files_chat_id_copilot_chats_id_fk": {
+ "name": "workspace_files_chat_id_copilot_chats_id_fk",
+ "tableFrom": "workspace_files",
+ "tableTo": "copilot_chats",
+ "columnsFrom": ["chat_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "workspace_files_organization_binding_check": {
+ "name": "workspace_files_organization_binding_check",
+ "value": "\"workspace_files\".\"organization_id\" IS NULL OR (\"workspace_files\".\"workspace_id\" IS NULL AND \"workspace_files\".\"context\" = 'knowledge-base' AND \"workspace_files\".\"folder_id\" IS NULL AND \"workspace_files\".\"chat_id\" IS NULL)"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.workspace_fork_block_map": {
+ "name": "workspace_fork_block_map",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "child_workspace_id": {
+ "name": "child_workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "parent_workflow_id": {
+ "name": "parent_workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "parent_block_id": {
+ "name": "parent_block_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "child_workflow_id": {
+ "name": "child_workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "child_block_id": {
+ "name": "child_block_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workspace_fork_block_map_child_ws_parent_unique": {
+ "name": "workspace_fork_block_map_child_ws_parent_unique",
+ "columns": [
+ {
+ "expression": "child_workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "parent_block_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_fork_block_map_child_ws_child_unique": {
+ "name": "workspace_fork_block_map_child_ws_child_unique",
+ "columns": [
+ {
+ "expression": "child_workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "child_block_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_fork_block_map_child_ws_parent_wf_idx": {
+ "name": "workspace_fork_block_map_child_ws_parent_wf_idx",
+ "columns": [
+ {
+ "expression": "child_workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "parent_workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_fork_block_map_child_ws_child_wf_idx": {
+ "name": "workspace_fork_block_map_child_ws_child_wf_idx",
+ "columns": [
+ {
+ "expression": "child_workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "child_workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workspace_fork_block_map_child_workspace_id_workspace_id_fk": {
+ "name": "workspace_fork_block_map_child_workspace_id_workspace_id_fk",
+ "tableFrom": "workspace_fork_block_map",
+ "tableTo": "workspace",
+ "columnsFrom": ["child_workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workspace_fork_dependent_value": {
+ "name": "workspace_fork_dependent_value",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "child_workspace_id": {
+ "name": "child_workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "target_workflow_id": {
+ "name": "target_workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "target_block_id": {
+ "name": "target_block_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "sub_block_key": {
+ "name": "sub_block_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workspace_fork_dependent_value_child_ws_wf_idx": {
+ "name": "workspace_fork_dependent_value_child_ws_wf_idx",
+ "columns": [
+ {
+ "expression": "child_workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "target_workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_fork_dependent_value_field_unique": {
+ "name": "workspace_fork_dependent_value_field_unique",
+ "columns": [
+ {
+ "expression": "child_workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "target_workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "target_block_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "sub_block_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workspace_fork_dependent_value_child_workspace_id_workspace_id_fk": {
+ "name": "workspace_fork_dependent_value_child_workspace_id_workspace_id_fk",
+ "tableFrom": "workspace_fork_dependent_value",
+ "tableTo": "workspace",
+ "columnsFrom": ["child_workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workspace_fork_promote_run": {
+ "name": "workspace_fork_promote_run",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "child_workspace_id": {
+ "name": "child_workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_workspace_id": {
+ "name": "source_workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "target_workspace_id": {
+ "name": "target_workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "direction": {
+ "name": "direction",
+ "type": "workspace_fork_promote_direction",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "snapshot": {
+ "name": "snapshot",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workspace_fork_promote_run_child_ws_target_unique": {
+ "name": "workspace_fork_promote_run_child_ws_target_unique",
+ "columns": [
+ {
+ "expression": "child_workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "target_workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_fork_promote_run_target_ws_idx": {
+ "name": "workspace_fork_promote_run_target_ws_idx",
+ "columns": [
+ {
+ "expression": "target_workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workspace_fork_promote_run_child_workspace_id_workspace_id_fk": {
+ "name": "workspace_fork_promote_run_child_workspace_id_workspace_id_fk",
+ "tableFrom": "workspace_fork_promote_run",
+ "tableTo": "workspace",
+ "columnsFrom": ["child_workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workspace_fork_promote_run_created_by_user_id_fk": {
+ "name": "workspace_fork_promote_run_created_by_user_id_fk",
+ "tableFrom": "workspace_fork_promote_run",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workspace_fork_resource_map": {
+ "name": "workspace_fork_resource_map",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "child_workspace_id": {
+ "name": "child_workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "resource_type": {
+ "name": "resource_type",
+ "type": "workspace_fork_resource_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "parent_resource_id": {
+ "name": "parent_resource_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "child_resource_id": {
+ "name": "child_resource_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workspace_fork_resource_map_child_ws_idx": {
+ "name": "workspace_fork_resource_map_child_ws_idx",
+ "columns": [
+ {
+ "expression": "child_workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_fork_resource_map_child_ws_type_idx": {
+ "name": "workspace_fork_resource_map_child_ws_type_idx",
+ "columns": [
+ {
+ "expression": "child_workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "resource_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_fork_resource_map_child_type_parent_unique": {
+ "name": "workspace_fork_resource_map_child_type_parent_unique",
+ "columns": [
+ {
+ "expression": "child_workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "resource_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "parent_resource_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workspace_fork_resource_map_child_workspace_id_workspace_id_fk": {
+ "name": "workspace_fork_resource_map_child_workspace_id_workspace_id_fk",
+ "tableFrom": "workspace_fork_resource_map",
+ "tableTo": "workspace",
+ "columnsFrom": ["child_workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workspace_fork_resource_map_created_by_user_id_fk": {
+ "name": "workspace_fork_resource_map_created_by_user_id_fk",
+ "tableFrom": "workspace_fork_resource_map",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workspace_operation_receipt": {
+ "name": "workspace_operation_receipt",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "request_id": {
+ "name": "request_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "request_hash": {
+ "name": "request_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "kind": {
+ "name": "kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "report": {
+ "name": "report",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workspace_operation_receipt_request_unique": {
+ "name": "workspace_operation_receipt_request_unique",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "request_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_operation_receipt_workspace_created_idx": {
+ "name": "workspace_operation_receipt_workspace_created_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workspace_operation_receipt_workspace_id_workspace_id_fk": {
+ "name": "workspace_operation_receipt_workspace_id_workspace_id_fk",
+ "tableFrom": "workspace_operation_receipt",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workspace_sandbox": {
+ "name": "workspace_sandbox",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "language": {
+ "name": "language",
+ "type": "sandbox_language",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "dependencies": {
+ "name": "dependencies",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "cli_tools": {
+ "name": "cli_tools",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "system_packages": {
+ "name": "system_packages",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "spec_hash": {
+ "name": "spec_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workspace_sandbox_workspace_name_unique": {
+ "name": "workspace_sandbox_workspace_name_unique",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_sandbox_workspace_idx": {
+ "name": "workspace_sandbox_workspace_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_sandbox_spec_hash_idx": {
+ "name": "workspace_sandbox_spec_hash_idx",
+ "columns": [
+ {
+ "expression": "spec_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workspace_sandbox_workspace_id_workspace_id_fk": {
+ "name": "workspace_sandbox_workspace_id_workspace_id_fk",
+ "tableFrom": "workspace_sandbox",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workspace_sandbox_created_by_user_id_fk": {
+ "name": "workspace_sandbox_created_by_user_id_fk",
+ "tableFrom": "workspace_sandbox",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {
+ "public.academy_cert_status": {
+ "name": "academy_cert_status",
+ "schema": "public",
+ "values": ["active", "revoked", "expired"]
+ },
+ "public.background_work_kind": {
+ "name": "background_work_kind",
+ "schema": "public",
+ "values": ["deployment_side_effects", "fork_content_copy", "fork_sync", "fork_rollback"]
+ },
+ "public.background_work_status_value": {
+ "name": "background_work_status_value",
+ "schema": "public",
+ "values": ["pending", "processing", "completed", "completed_with_warnings", "failed"]
+ },
+ "public.billing_blocked_reason": {
+ "name": "billing_blocked_reason",
+ "schema": "public",
+ "values": ["payment_failed", "dispute"]
+ },
+ "public.billing_entity_type": {
+ "name": "billing_entity_type",
+ "schema": "public",
+ "values": ["user", "organization"]
+ },
+ "public.chat_type": {
+ "name": "chat_type",
+ "schema": "public",
+ "values": ["mothership", "copilot"]
+ },
+ "public.copilot_async_tool_status": {
+ "name": "copilot_async_tool_status",
+ "schema": "public",
+ "values": ["pending", "running", "completed", "failed", "cancelled", "delivered"]
+ },
+ "public.copilot_run_status": {
+ "name": "copilot_run_status",
+ "schema": "public",
+ "values": ["active", "paused_waiting_for_tool", "resuming", "complete", "error", "cancelled"]
+ },
+ "public.copilot_tool_permission_decision": {
+ "name": "copilot_tool_permission_decision",
+ "schema": "public",
+ "values": ["allow", "allow_chat", "always_allow", "skip"]
+ },
+ "public.credential_group_enrollment_status": {
+ "name": "credential_group_enrollment_status",
+ "schema": "public",
+ "values": ["invited", "delivery_failed", "in_progress", "completed", "revoked"]
+ },
+ "public.credential_group_status": {
+ "name": "credential_group_status",
+ "schema": "public",
+ "values": ["active", "disabled"]
+ },
+ "public.credential_member_role": {
+ "name": "credential_member_role",
+ "schema": "public",
+ "values": ["admin", "member"]
+ },
+ "public.credential_member_status": {
+ "name": "credential_member_status",
+ "schema": "public",
+ "values": ["active", "pending", "revoked"]
+ },
+ "public.credential_type": {
+ "name": "credential_type",
+ "schema": "public",
+ "values": [
+ "oauth",
+ "managed_oauth",
+ "managed_mcp",
+ "env_workspace",
+ "env_personal",
+ "service_account",
+ "personal_token"
+ ]
+ },
+ "public.data_drain_cadence": {
+ "name": "data_drain_cadence",
+ "schema": "public",
+ "values": ["hourly", "daily"]
+ },
+ "public.data_drain_destination": {
+ "name": "data_drain_destination",
+ "schema": "public",
+ "values": ["s3", "gcs", "azure_blob", "datadog", "bigquery", "snowflake", "webhook"]
+ },
+ "public.data_drain_run_status": {
+ "name": "data_drain_run_status",
+ "schema": "public",
+ "values": ["running", "success", "failed"]
+ },
+ "public.data_drain_run_trigger": {
+ "name": "data_drain_run_trigger",
+ "schema": "public",
+ "values": ["cron", "manual"]
+ },
+ "public.data_drain_source": {
+ "name": "data_drain_source",
+ "schema": "public",
+ "values": ["workflow_logs", "job_logs", "audit_logs", "copilot_chats", "copilot_runs"]
+ },
+ "public.execution_large_value_reference_source": {
+ "name": "execution_large_value_reference_source",
+ "schema": "public",
+ "values": ["execution_log", "paused_snapshot"]
+ },
+ "public.folder_resource_type": {
+ "name": "folder_resource_type",
+ "schema": "public",
+ "values": ["workflow", "file", "knowledge_base", "table"]
+ },
+ "public.invitation_kind": {
+ "name": "invitation_kind",
+ "schema": "public",
+ "values": ["organization", "workspace"]
+ },
+ "public.invitation_membership_intent": {
+ "name": "invitation_membership_intent",
+ "schema": "public",
+ "values": ["internal", "external"]
+ },
+ "public.invitation_status": {
+ "name": "invitation_status",
+ "schema": "public",
+ "values": ["pending", "accepted", "rejected", "cancelled", "expired"]
+ },
+ "public.managed_oauth_credential_status": {
+ "name": "managed_oauth_credential_status",
+ "schema": "public",
+ "values": ["active", "needs_reauth", "revoked"]
+ },
+ "public.permission_type": {
+ "name": "permission_type",
+ "schema": "public",
+ "values": ["admin", "write", "read"]
+ },
+ "public.sandbox_image_status": {
+ "name": "sandbox_image_status",
+ "schema": "public",
+ "values": ["pending", "building", "ready", "failed"]
+ },
+ "public.sandbox_language": {
+ "name": "sandbox_language",
+ "schema": "public",
+ "values": ["javascript", "python"]
+ },
+ "public.secret_usage_scope": {
+ "name": "secret_usage_scope",
+ "schema": "public",
+ "values": ["workspace", "personal"]
+ },
+ "public.secret_usage_source": {
+ "name": "secret_usage_source",
+ "schema": "public",
+ "values": ["workflow", "copilot", "mcp"]
+ },
+ "public.upload_session_method": {
+ "name": "upload_session_method",
+ "schema": "public",
+ "values": ["put", "multipart"]
+ },
+ "public.upload_session_provider": {
+ "name": "upload_session_provider",
+ "schema": "public",
+ "values": ["local", "s3", "blob", "gcs"]
+ },
+ "public.upload_session_purpose": {
+ "name": "upload_session_purpose",
+ "schema": "public",
+ "values": [
+ "workspace_file",
+ "table_import",
+ "knowledge_document",
+ "profile_picture",
+ "workspace_logo",
+ "mothership_attachment",
+ "execution_attachment"
+ ]
+ },
+ "public.upload_session_status": {
+ "name": "upload_session_status",
+ "schema": "public",
+ "values": [
+ "uploading",
+ "completing",
+ "finalizing",
+ "completed",
+ "aborting",
+ "aborted",
+ "failed",
+ "expired"
+ ]
+ },
+ "public.usage_log_category": {
+ "name": "usage_log_category",
+ "schema": "public",
+ "values": ["model", "fixed", "tool", "model_unbilled"]
+ },
+ "public.usage_log_source": {
+ "name": "usage_log_source",
+ "schema": "public",
+ "values": [
+ "workflow",
+ "wand",
+ "copilot",
+ "workspace-chat",
+ "mcp_copilot",
+ "mothership_block",
+ "knowledge-base",
+ "voice-input",
+ "enrichment",
+ "voice-output",
+ "api-tool"
+ ]
+ },
+ "public.workspace_file_search_index_status": {
+ "name": "workspace_file_search_index_status",
+ "schema": "public",
+ "values": ["pending", "ready", "skipped", "failed"]
+ },
+ "public.workspace_fork_promote_direction": {
+ "name": "workspace_fork_promote_direction",
+ "schema": "public",
+ "values": ["push", "pull"]
+ },
+ "public.workspace_fork_resource_type": {
+ "name": "workspace_fork_resource_type",
+ "schema": "public",
+ "values": [
+ "workflow",
+ "oauth_credential",
+ "service_account_credential",
+ "env_var",
+ "table",
+ "knowledge_base",
+ "knowledge_document",
+ "file",
+ "file_folder",
+ "mcp_server",
+ "workflow_mcp_server",
+ "custom_block",
+ "custom_tool",
+ "skill",
+ "sandbox"
+ ]
+ },
+ "public.workspace_mode": {
+ "name": "workspace_mode",
+ "schema": "public",
+ "values": ["personal", "organization", "grandfathered_shared"]
+ }
+ },
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json
index 660164f99d0..f8f91eff92f 100644
--- a/packages/db/migrations/meta/_journal.json
+++ b/packages/db/migrations/meta/_journal.json
@@ -2360,6 +2360,13 @@
"when": 1788986020651,
"tag": "0337_colossal_the_renegades",
"breakpoints": true
+ },
+ {
+ "idx": 338,
+ "version": "7",
+ "when": 1789076658450,
+ "tag": "0338_organization_search_activity",
+ "breakpoints": true
}
]
}
diff --git a/packages/db/schema.ts b/packages/db/schema.ts
index 89375ccdb66..8d1b2746844 100644
--- a/packages/db/schema.ts
+++ b/packages/db/schema.ts
@@ -4550,6 +4550,37 @@ export const usageLogSourceEnum = pgEnum('usage_log_source', [
'api-tool',
])
+/** Content-free organization Search activity, independent of billable model usage. */
+export const organizationSearchInvocation = pgTable(
+ 'organization_search_invocation',
+ {
+ id: text('id').primaryKey(),
+ organizationId: text('organization_id')
+ .notNull()
+ .references(() => organization.id, { onDelete: 'cascade' }),
+ userId: text('user_id').references(() => user.id, { onDelete: 'set null' }),
+ surface: text('surface').notNull(),
+ sourceTypes: text('source_types').array().notNull(),
+ resultCount: integer('result_count').notNull(),
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
+ },
+ (table) => ({
+ organizationCreatedAtIdx: index('organization_search_invocation_org_created_idx').on(
+ table.organizationId,
+ table.createdAt
+ ),
+ userIdIdx: index('organization_search_invocation_user_idx').on(table.userId),
+ resultCountBounds: check(
+ 'organization_search_invocation_result_count_bounds',
+ sql`${table.resultCount} BETWEEN 0 AND 100`
+ ),
+ sourceTypesBounds: check(
+ 'organization_search_invocation_source_types_bounds',
+ sql`cardinality(${table.sourceTypes}) <= 100`
+ ),
+ })
+)
+
export const usageLog = pgTable(
'usage_log',
{
diff --git a/packages/emcn/src/components/combobox/combobox.dom.test.tsx b/packages/emcn/src/components/combobox/combobox.dom.test.tsx
index 958fc8bbb38..f410e4a96ca 100644
--- a/packages/emcn/src/components/combobox/combobox.dom.test.tsx
+++ b/packages/emcn/src/components/combobox/combobox.dom.test.tsx
@@ -76,6 +76,28 @@ afterEach(() => {
})
describe('Combobox onOpenChange', () => {
+ it.each([false, true])(
+ 'puts the field name and validation ARIA on the interactive control (editable=%s)',
+ (editable) => {
+ render(
+
+ )
+ const control = trigger()
+ expect(control.getAttribute('aria-label')).toBe('Projects')
+ expect(control.getAttribute('aria-describedby')).toBe('projects-hint')
+ expect(control.getAttribute('aria-required')).toBe('true')
+ expect(control.getAttribute('aria-invalid')).toBe('true')
+ expect(control.closest('[aria-label="Projects"]')).toBe(control)
+ }
+ )
+
it('renders the dropdown inside the component subtree when portals are disabled', () => {
render( )
diff --git a/packages/emcn/src/components/combobox/combobox.tsx b/packages/emcn/src/components/combobox/combobox.tsx
index ddf79d53e71..39858f47f67 100644
--- a/packages/emcn/src/components/combobox/combobox.tsx
+++ b/packages/emcn/src/components/combobox/combobox.tsx
@@ -59,6 +59,8 @@ export type ComboboxOption = {
iconElement?: ReactNode
/** Custom select handler - when provided, this is called instead of onChange */
onSelect?: () => void
+ /** Explicit selection state for action options whose values are not stored in the selection. */
+ selected?: boolean
/** Whether this option is disabled */
disabled?: boolean
/** When true, keep the dropdown open after selecting this option */
@@ -222,6 +224,11 @@ const Combobox = memo(
searchActive = false,
onLoadMore,
onLoadAll,
+ 'aria-label': ariaLabel,
+ 'aria-labelledby': ariaLabelledBy,
+ 'aria-describedby': ariaDescribedBy,
+ 'aria-required': ariaRequired,
+ 'aria-invalid': ariaInvalid,
...props
},
ref
@@ -733,9 +740,11 @@ const Combobox = memo(
) : null
const renderFlatOption = (option: ComboboxOption, index: number) => {
- const isSelected = multiSelect
- ? multiSelectValues?.includes(option.value)
- : effectiveSelectedValue === option.value
+ const isSelected =
+ option.selected ??
+ (multiSelect
+ ? multiSelectValues?.includes(option.value)
+ : effectiveSelectedValue === option.value)
const isHighlighted = index === effectiveHighlightedIndex
const OptionIcon = option.icon
@@ -795,6 +804,11 @@ const Combobox = memo(
onBlur={handleBlur}
onKeyDown={handleKeyDown}
disabled={disabled}
+ aria-label={ariaLabel}
+ aria-labelledby={ariaLabelledBy}
+ aria-describedby={ariaDescribedBy}
+ aria-required={ariaRequired}
+ aria-invalid={ariaInvalid}
{...inputProps}
role='combobox'
aria-expanded={open}
@@ -840,6 +854,11 @@ const Combobox = memo(
- ) : error && filteredOptions.length === 0 && !hasMore ? (
+ ) : error && filteredOptions.length === 0 ? (
{error}
@@ -994,9 +1013,11 @@ const Combobox = memo(
)}
{group.items.map((option) => {
- const isSelected = multiSelect
- ? multiSelectValues?.includes(option.value)
- : effectiveSelectedValue === option.value
+ const isSelected =
+ option.selected ??
+ (multiSelect
+ ? multiSelectValues?.includes(option.value)
+ : effectiveSelectedValue === option.value)
const globalIndex = filteredOptions.findIndex(
(o) => o.value === option.value
)
diff --git a/packages/emcn/src/components/index.ts b/packages/emcn/src/components/index.ts
index 039102ac8c2..5505719f2af 100644
--- a/packages/emcn/src/components/index.ts
+++ b/packages/emcn/src/components/index.ts
@@ -68,6 +68,7 @@ export {
ChipModalError,
type ChipModalErrorProps,
ChipModalField,
+ type ChipModalFieldAria,
type ChipModalFieldProps,
ChipModalFooter,
type ChipModalFooterAction,
diff --git a/packages/testing/src/mocks/schema.mock.ts b/packages/testing/src/mocks/schema.mock.ts
index 5f7427e14b9..8d951f74c6c 100644
--- a/packages/testing/src/mocks/schema.mock.ts
+++ b/packages/testing/src/mocks/schema.mock.ts
@@ -1629,6 +1629,15 @@ export const schemaMock = {
approved: 'organizationSearchIntegration.approved',
updatedAt: 'organizationSearchIntegration.updatedAt',
},
+ organizationSearchInvocation: {
+ id: 'organizationSearchInvocation.id',
+ organizationId: 'organizationSearchInvocation.organizationId',
+ userId: 'organizationSearchInvocation.userId',
+ surface: 'organizationSearchInvocation.surface',
+ sourceTypes: 'organizationSearchInvocation.sourceTypes',
+ resultCount: 'organizationSearchInvocation.resultCount',
+ createdAt: 'organizationSearchInvocation.createdAt',
+ },
knowledgeConnector: {
id: 'knowledgeConnector.id',
knowledgeBaseId: 'knowledgeConnector.knowledgeBaseId',