From 6dcc65be899804447aa4862ba82ebef60498baba Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 23 Jul 2026 19:52:00 -0700 Subject: [PATCH] feat(skills): add skill editors (#5705) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(skills): permissions layer * chore(db): drop skill_member migration 0261 for regeneration on latest staging Co-Authored-By: Claude Fable 5 * feat(db): regenerate skill_member migration as 0262 on latest staging Same DDL as the dropped 0261 (skill_member table, enums, indexes, skill.workspace_shared) plus the hand-written write-user backfill, renumbered after staging's 0261. Co-Authored-By: Claude Fable 5 * feat(db): regenerate skill_member migration as 0263 after staging merge Staging claimed 0262 (strong_storm); same DDL plus the hand-written write-user backfill, renumbered on the merged snapshot chain. Co-Authored-By: Claude Fable 5 * chore(db): drop skill_member migration 0263 for regeneration on latest staging Co-Authored-By: Claude Fable 5 * feat(db): regenerate skill_member migration as 0264 after staging merge Staging claimed 0263 (workflow_fork_sync_excluded); same DDL plus the hand-written write-user backfill, renumbered on the merged snapshot chain. Co-Authored-By: Claude Fable 5 * make editing skills full page * fix disclaimer * edit access msg * fix lint * chore(db): drop skill_member migration 0264 for regeneration on latest staging Co-Authored-By: Claude Fable 5 * feat(db): regenerate skill_member migration as 0265 after staging merge Staging claimed 0264 (fat_ikaris); same DDL plus the hand-written write-user backfill, renumbered on the merged snapshot chain. Co-Authored-By: Claude Fable 5 * fix tests * simplify system * fix * fix lint * add mship skills docs * chore(db): drop skill_member migration 0265 for regeneration on latest staging Co-Authored-By: Claude Fable 5 * feat(db): regenerate skill_member migration as 0266 after staging merge Co-Authored-By: Claude Fable 5 * fix(deps): override zod to 4.3.6 to dedupe nested copies breaking type-check better-auth 1.6.23 and fumadocs-mdx resolve ^4.3.6 to a nested zod 4.4.3, which makes @sim/auth's inferred betterAuth types non-portable (TS2883) and split docs onto a second zod instance. Both ranges accept the repo-wide pinned 4.3.6, so a single hoisted copy satisfies everything. Co-Authored-By: Claude Fable 5 * fix lint * feat(skills,tools): fullscreen skill create + shared custom tool editor Moves the rich-markdown and custom-tool editing surfaces out of modals and onto full-page surfaces, and collapses the duplicated chrome behind shared components. Skills - Add /skills/new, a full-page create surface mirroring the skill detail page (CredentialDetailLayout + DetailSection + unsaved-changes guard). "Add to Sim" navigates there instead of opening a modal. - Import moves to a header action (SkillImportButton) backed by a shared readSkillFile helper; the GitHub-URL import and its /api/skills/import route are removed. - Skill name validation is now one shared validateSkillName, replacing three copies of the kebab-case rule and its messages. - The skill editor roster renders through the shared MemberRow instead of re-deriving its identity block, with a locked role control and a lock-reason tooltip explaining inherited workspace-admin access. Custom tools - Extract the canvas modal's schema/code editors into a shared custom-tool-editor module (fields, wand generation, schema helpers), cutting custom-tool-modal.tsx by ~900 lines. - Settings > Custom tools gains a full-page detail sub-view (SettingsPanel + SettingsSection + saveDiscardActions), deep-linkable via ?custom-tool-id. Rows are clickable; delete now lives only in the detail view. - Replace legacy Button/Input/Badge/Label with the chip family, move chip-field chrome into CodeEditor behind an error prop, and delete its dead wand button. Rich markdown field - maxHeight is now opt-in: omit it on a page and the editor grows with its content so the page owns the only scrollbar. Modals pass explicit caps. - The field variant drops to font-weight 400 to match adjacent chip fields. * fix(skills): address review round on create navigation, 409 copy, and editor audit - Skill create navigated using the first element of the upsert response, but that endpoint returns the caller's whole skill list (built-ins prepended) — match the new skill by its workspace-unique name instead. - The suggested-skill 409 toast claimed the skill existed but was not shared and told the user to ask a skill admin. Every workspace member can already see and use every skill, so a 409 only means the name is taken. - Adding an editor emitted the skill_shared event and SKILL_MEMBER_ADDED audit even when onConflictDoNothing skipped the insert on a concurrent add. Gate both on the insert actually returning a row. * chore: format skills-resolver test import * fix(skills,tools): audit fixes — autocomplete boundary, resize clipping, error routing Two real regressions introduced while simplifying the extracted editor: - The schema-param autocomplete's trigger was rewritten to match a trailing identifier, but the completion still split on separators. The two disagreed, so typing `data.ci` opened the menu and selecting replaced `data.ci` whole — eating the member-access prefix. Both now share one SCHEMA_PARAM_WORD regex. - The uncapped markdown field measured its height only on value change while always setting overflow-hidden, so any width change that re-wrapped lines clipped the tail with no scrollbar to reach it. Now re-measures via ResizeObserver. Also from the audit: - Generation writes bypass the code field's change handler, so an open autocomplete stayed over a disabled streaming editor; close it on busy. - Delete failures rendered in the Schema section's error slot on both custom tool surfaces; route them to a toast instead. - Skill create navigated away while still dirty, stranding the unsaved-changes guard's history sentinel so Back landed on an empty create form. - The Description field on skill create never received its error border. - Drop a double-applied opacity-50 (the editor already dims when disabled), a dead try/catch around a non-throwing call that also shadowed the error prop, and a stale reference to a /tools page that does not exist. - Docs still described the removed GitHub-URL import and the old Add Skill dialog; rewrite for the create page and file/paste import. * feat(tools): read-only tool detail, create lands on the new tool, drop dead wand prompt API - Viewers without edit rights could not open a custom tool at all, while the equivalent skill and custom-block surfaces both offer a read-only view. The detail page now takes `readOnly`: editors inert, no Save/Discard/Delete, no Generate. Creating still requires edit rights. - Creating a tool bounced back to the list while creating a skill lands on the new skill. Tools now do the same. The upsert returns the workspace's whole tool list (newest first) rather than just the new row, so the id is matched by title instead of by index — the same trap that produced the skill-create navigation bug. - Remove `openPrompt`/`closePrompt` from useWand. `closePrompt`'s last callers went away with the custom-tool-modal extraction and `openPrompt` had none before it; nothing reads `isPromptVisible` any more either. * fix(tools): read-only editors, design-system wrench, skills-matching tool identity - readOnly never reached the editors: the prop gated actions and Generate but the schema and code fields were still typable for viewers without edit rights. Wire disabled through both fields into CodeEditor. - The row icon used lucide's Wrench (strokeWidth 2) where @sim/emcn/icons ships one drawn for this system (1.55, tuned viewBox), and it inherited body text colour instead of --text-icon. Swap it. - Give the tool detail page the same identity heading as skill detail: tile, name, and description at the top left, instead of only a header title. - Extract ResourceTile so the skills and tools tiles share one definition (SkillTile now composes it), and add an opt-in `iconFilled` to SettingsResourceRow so the tools list tile matches the skills gallery. Both default to today's behaviour for every existing consumer. * fix(mentions): use the product's own glyph for every @ mention kind The `@` menu and the inserted chip mapped kinds to arbitrary lucide icons — `Sparkles` for a skill, a generic `File` for every file — while the rest of the product has a settled glyph per resource. Mirror CHAT_CONTEXT_KIND_REGISTRY, which Chat's `@` menu already renders from: - skill now uses AgentSkillsIcon, the same glyph SkillTile shows everywhere - workflow / folder / table / knowledge use the @sim/emcn/icons set the sidebar and the chat registry use - file derives its icon from the filename extension, so a .pdf and a .csv are distinguishable, matching the file list and Chat's context chips - integration keeps the block's brand icon from the registry Also drop the generic placeholder. `kind` is untrusted — the node schema defaults it to `''` and a hand-written `sim:` link can carry anything — but an unrecognized kind now yields no icon instead of a meaningless box, which is what the chat registry does. The menu already guarded a missing icon; the chip now does too, so this cannot crash on a malformed link. * chore(db): drop skill_member migration 0266 for regeneration on latest staging * feat(db): regenerate skill_member migration as 0267 after staging merge --------- Co-authored-by: Claude Fable 5 Co-authored-by: Waleed Latif --- apps/docs/content/docs/en/agents/skills.mdx | 37 +- apps/sim/app/api/mothership/execute/route.ts | 61 +- apps/sim/app/api/skills/[id]/members/route.ts | 251 + apps/sim/app/api/skills/import/route.ts | 103 - apps/sim/app/api/skills/route.ts | 76 +- .../[id]/members/[memberId]/route.ts | 2 + .../v1/admin/workspaces/[id]/members/route.ts | 2 + .../app/api/workspaces/members/[id]/route.ts | 2 + .../components/add-people-modal.tsx | 152 +- .../components/credential-members-section.tsx | 73 +- .../components/credential-detail/roles.ts | 15 - .../custom-tool-code-field.tsx | 380 + .../custom-tool-schema-field.tsx | 41 + .../custom-tool-editor/custom-tool-schema.ts | 126 + .../custom-tool-editor/field-error-text.tsx | 10 + .../generate-prompt-control.tsx | 90 + .../components/custom-tool-editor/index.ts | 15 + .../use-custom-tool-generation.ts | 193 + .../[workspaceId]/components/index.ts | 1 + .../components/resource-tile/index.ts | 1 + .../resource-tile/resource-tile.tsx | 20 + .../components/skill-tile/skill-tile.tsx | 9 +- .../mention/mention-chip.tsx | 6 +- .../mention/mention-icon.test.ts | 24 +- .../mention/mention-icon.ts | 42 +- .../mention/use-markdown-mentions.ts | 2 +- .../rich-markdown-editor.css | 6 +- .../rich-markdown-field.tsx | 69 +- .../[block]/integration-skills-section.tsx | 12 +- .../settings/[section]/search-params.ts | 16 + .../custom-tool-detail/custom-tool-detail.tsx | 336 + .../components/custom-tool-detail/index.ts | 1 + .../components/custom-tools/custom-tools.tsx | 256 +- .../settings-resource-row.tsx | 16 +- .../components/skill-editors-card.tsx | 59 + .../[workspaceId]/skills/[skillId]/page.tsx | 15 + .../skills/[skillId]/skill-detail.tsx | 356 + .../skills/components/skill-import/index.ts | 1 + .../skill-import/skill-import-button.tsx | 58 + .../components/skill-import/skill-import.tsx | 170 +- .../skills/components/skill-members/index.ts | 4 + .../use-skill-editors-controller.ts | 76 + .../components/skill-modal/skill-modal.tsx | 71 +- .../[workspaceId]/skills/components/utils.ts | 57 +- .../[workspaceId]/skills/new/page.tsx | 15 + .../[workspaceId]/skills/new/skill-create.tsx | 237 + .../[workspaceId]/skills/search-params.ts | 12 +- .../workspace/[workspaceId]/skills/skills.tsx | 111 +- .../components/skill-input/skill-input.tsx | 23 +- .../components/code-editor/code-editor.tsx | 38 +- .../custom-tool-modal/custom-tool-modal.tsx | 952 +- .../w/[workflowId]/hooks/use-wand.ts | 13 - .../permissions/add-people-modal.tsx | 171 + apps/sim/components/permissions/index.ts | 10 + .../permissions/member-role-options.ts | 28 + .../sim/components/permissions/member-row.tsx | 90 + apps/sim/components/permissions/role-lock.tsx | 9 + .../lib/copy/copy-resources.test.ts | 71 +- .../lib/copy/copy-resources.ts | 46 +- .../handlers/agent/skills-resolver.test.ts | 58 +- .../handlers/agent/skills-resolver.ts | 8 +- apps/sim/executor/handlers/pi/context.ts | 15 +- apps/sim/executor/handlers/pi/pi-handler.ts | 1 - apps/sim/hooks/queries/skills.ts | 120 +- apps/sim/lib/api/contracts/skills.ts | 117 +- .../lib/billing/organizations/membership.ts | 4 + .../sim/lib/copilot/chat/workspace-context.ts | 23 +- .../tools/handlers/management/manage-skill.ts | 56 +- apps/sim/lib/copilot/vfs/workspace-vfs.ts | 8 +- apps/sim/lib/credentials/access.test.ts | 5 + apps/sim/lib/credentials/access.ts | 19 +- apps/sim/lib/mothership/inbox/executor.ts | 27 +- apps/sim/lib/posthog/events.ts | 10 + apps/sim/lib/skills/access.test.ts | 282 + apps/sim/lib/skills/access.ts | 212 + .../lib/workflows/skills/operations.test.ts | 86 +- apps/sim/lib/workflows/skills/operations.ts | 146 +- apps/sim/lib/workspaces/permissions/utils.ts | 15 + .../workspaces}/sharing.test.ts | 2 +- .../workspaces}/sharing.ts | 5 +- bun.lock | 30 +- package.json | 3 +- packages/audit/src/types.ts | 2 + packages/db/migrations/0267_dark_romulus.sql | 29 + .../db/migrations/meta/0267_snapshot.json | 17529 ++++++++++++++++ packages/db/migrations/meta/_journal.json | 7 + packages/db/schema.ts | 25 + .../components/chip-switch/chip-switch.tsx | 2 +- packages/testing/src/mocks/audit.mock.ts | 2 + packages/testing/src/mocks/schema.mock.ts | 8 + scripts/check-api-validation-contracts.ts | 4 +- scripts/check-migrations-safety.ts | 5 +- 92 files changed, 22050 insertions(+), 1954 deletions(-) create mode 100644 apps/sim/app/api/skills/[id]/members/route.ts delete mode 100644 apps/sim/app/api/skills/import/route.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/components/credential-detail/roles.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/components/custom-tool-editor/custom-tool-code-field.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/components/custom-tool-editor/custom-tool-schema-field.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/components/custom-tool-editor/custom-tool-schema.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/components/custom-tool-editor/field-error-text.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/components/custom-tool-editor/generate-prompt-control.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/components/custom-tool-editor/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/components/custom-tool-editor/use-custom-tool-generation.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/components/resource-tile/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/components/resource-tile/resource-tile.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/settings/components/custom-tools/components/custom-tool-detail/custom-tool-detail.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/settings/components/custom-tools/components/custom-tool-detail/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/skills/[skillId]/components/skill-editors-card.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/skills/[skillId]/page.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/skills/[skillId]/skill-detail.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/skills/components/skill-import/skill-import-button.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/skills/components/skill-members/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/skills/components/skill-members/use-skill-editors-controller.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/skills/new/page.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/skills/new/skill-create.tsx create mode 100644 apps/sim/components/permissions/add-people-modal.tsx create mode 100644 apps/sim/components/permissions/member-role-options.ts create mode 100644 apps/sim/components/permissions/member-row.tsx create mode 100644 apps/sim/lib/skills/access.test.ts create mode 100644 apps/sim/lib/skills/access.ts rename apps/sim/{app/workspace/[workspaceId]/components/credential-detail => lib/workspaces}/sharing.test.ts (96%) rename apps/sim/{app/workspace/[workspaceId]/components/credential-detail => lib/workspaces}/sharing.ts (88%) create mode 100644 packages/db/migrations/0267_dark_romulus.sql create mode 100644 packages/db/migrations/meta/0267_snapshot.json diff --git a/apps/docs/content/docs/en/agents/skills.mdx b/apps/docs/content/docs/en/agents/skills.mdx index 027ae0def9..bd608982d3 100644 --- a/apps/docs/content/docs/en/agents/skills.mdx +++ b/apps/docs/content/docs/en/agents/skills.mdx @@ -18,13 +18,11 @@ Skills use **progressive disclosure** to keep agent context lean: ## Creating Skills -Skills live on the **Integrations** page: click **Integrations** in the workspace sidebar, then switch to the **Skills** tab. It lists every skill in the workspace, searchable by name. +Skills live on the **Integrations** page: click **Integrations** in the workspace sidebar, then switch to the **Skills** tab. It lists every skill in the workspace, searchable by name. Click a skill to open its detail page, where you edit, share, and delete it. ![The Skills tab on the Integrations page](/static/skills/skills-tab.png) -Click **+ Add to Sim** to open the **Add Skill** dialog. The **Create** tab takes three fields: - -![The Add Skill dialog, Create tab](/static/skills/add-skill-create.png) +Click **+ Add to Sim** to open the skill create page, which takes three fields: | Field | Description | |-------|-------------| @@ -38,13 +36,10 @@ Click **+ Add to Sim** to open the **Add Skill** dialog. The **Create** tab take ### Importing skills -The **Import** tab brings in an existing skill in the open [SKILL.md](https://agentskills.io/specification) format, three ways: +Bring in an existing skill in the open [SKILL.md](https://agentskills.io/specification) format two ways: -![The Add Skill dialog, Import tab](/static/skills/add-skill-import.png) - -- **Upload a file** — a `.md` file with YAML frontmatter, or a `.zip` containing a `SKILL.md`. -- **Import from GitHub** — paste a GitHub URL to a `SKILL.md` and click **Fetch**. -- **Paste content** — paste the `SKILL.md` directly. The frontmatter carries the `name` and `description`; the markdown body is the content. +- **Import** — the **Import** action on the create page takes a `.md` file with YAML frontmatter, or a `.zip` containing a `SKILL.md`. +- **Paste content** — paste the `SKILL.md` straight into **Content**. The frontmatter carries the `name` and `description`; the markdown body is the content. Integration pages suggest **curated skills** for their service — open one (HubSpot, for example) and add a suggested skill with one click. @@ -77,6 +72,24 @@ Use when the user asks you to write, optimize, or debug SQL queries. Keep skills focused and under 500 lines. If a skill grows too large, split it into multiple specialized skills. +## Skill Editors + +Everyone in the workspace sees and uses every skill — including members who join later. Nobody needs to be added to a skill to use it. + +Each skill has an explicit **editors** list. Editors can edit the skill, delete it, and manage the editors list. Workspace admins can always do this too — they are editors of every skill automatically and cannot be removed from the list. Whoever creates a skill becomes an editor. + +Open a skill from the Skills tab to manage it. The detail page has the editable fields, a **Share** action for adding editors from your workspace members, and the **Skill Editors** list at the bottom. + + + The editors list controls who can edit a skill — it never affects who can see, use, or run it. A workflow that references a skill always executes it, no matter who runs the workflow. Treat skill content as shared team instructions, not as a secret. + + +## Using Skills in Chat + +Skills work in Chat too. Type `/` in the message box to open the skills menu, then pick a skill — or keep typing to filter by name. The skill appears in your message as a tag, e.g. `/format-markdown`. + +Tagging a skill loads its full instructions into the conversation, so Sim follows them for that request — no waiting for Sim to decide the skill is relevant on its own. + ## Adding Skills to an Agent Open any **Agent** block and find the **Skills** dropdown below the tools section. Select the skills you want the agent to have access to. @@ -156,5 +169,7 @@ import { FAQ } from '@/components/ui/faq' { question: "When should I use skills vs. agent instructions?", answer: "Use skills for knowledge that applies across multiple workflows or changes frequently. Skills are reusable packages that can be attached to any agent. Use agent instructions for task-specific context that is unique to a single agent and workflow. If you find yourself copying the same instructions into multiple agents, that content should be a skill instead." }, { question: "Can permission groups disable skills for certain users?", answer: "Yes. On Enterprise-entitled workspaces, any workspace admin can create a permission group with the disableSkills option enabled. When a user is assigned to such a group in a workspace, the skills dropdown in agent blocks is disabled and they cannot add or use skills in workflows belonging to that workspace." }, { question: "What is the recommended maximum length for skill content?", answer: "Keep skills focused and under 500 lines. If a skill grows too large, split it into multiple specialized skills. Shorter, focused skills are more effective because the agent can load exactly what it needs. A broad skill with too much content can overwhelm the agent and reduce the quality of its responses." }, - { question: "Where do I create and manage skills?", answer: "Click Integrations in the workspace sidebar and switch to the Skills tab. Add Skill creates one from a name (kebab-case, max 64 characters), description (max 1024 characters), and markdown content — or imports an existing SKILL.md from a file, a GitHub URL, or pasted content. Existing skills are edited and deleted from the same tab." }, + { question: "Where do I create and manage skills?", answer: "Click Integrations in the workspace sidebar and switch to the Skills tab. Add to Sim opens a create page taking a name (kebab-case, max 64 characters), description (max 1024 characters), and markdown content — or imports an existing SKILL.md from a file or pasted content. Click any existing skill to open its detail page, where you edit, share, and delete it." }, + { question: "Who can edit a skill, and who can use it?", answer: "Everyone in the workspace — including people who join later — sees and uses every skill without being added to anything. Each skill has an editors list: editors and workspace admins (who are always editors, automatically) can edit, delete, and share the skill, and whoever creates a skill becomes an editor. The editors list never affects who can use or run a skill: a workflow that references a skill always executes it." }, + { question: "Can I use skills in Chat?", answer: "Yes. Type / in the Chat message box to open the skills menu and pick a skill — for example /format-markdown. The tag loads the skill's full instructions into the conversation, so Sim follows them for that request without having to decide on its own to load the skill." }, ]} /> diff --git a/apps/sim/app/api/mothership/execute/route.ts b/apps/sim/app/api/mothership/execute/route.ts index 9904802b64..36641a775f 100644 --- a/apps/sim/app/api/mothership/execute/route.ts +++ b/apps/sim/app/api/mothership/execute/route.ts @@ -22,7 +22,6 @@ import { isDocSandboxEnabled } from '@/lib/core/config/env-flags' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { assertActiveWorkspaceAccess, - getUserEntityPermissions, isWorkspaceAccessDeniedError, } from '@/lib/workspaces/permissions/utils' import type { ChatContext } from '@/stores/panel' @@ -130,7 +129,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => { } const userId = auth.userId ?? bodyUserId - await assertActiveWorkspaceAccess(workspaceId, userId) + const workspaceAccess = await assertActiveWorkspaceAccess(workspaceId, userId) const billingAttribution = requireBillingAttributionHeader(req.headers, { actorUserId: userId, workspaceId, @@ -152,38 +151,32 @@ export const POST = withRouteHandler(async (req: NextRequest) => { context.kind === 'mcp' && context.serverId ? [context.serverId] : [] ) const nonMcpAgentMentions = agentMentions?.filter((context) => context.kind !== 'mcp') - const [ - workspaceContext, - integrationTools, - mothershipTools, - userPermission, - entitlements, - agentContexts, - ] = await Promise.all([ - generateWorkspaceContext(workspaceId, userId), - buildIntegrationToolSchemas(userId, messageId, undefined, workspaceId), - Promise.all([ - buildSelectedMcpToolSchemas(userId, workspaceId, mcpTools ?? []), - buildTaggedMcpToolSchemas(userId, workspaceId, taggedMcpServerIds), - ]).then((groups) => { - const byName = new Map(groups.flat().map((tool) => [tool.name, tool])) - return [...byName.values()] - }), - getUserEntityPermissions(userId, 'workspace', workspaceId).catch(() => null), - computeWorkspaceEntitlements(workspaceId, userId), - processContextsServer( - nonMcpAgentMentions, - userId, - lastUserMessage, - workspaceId, - effectiveChatId - ).catch((error) => { - reqLogger.warn('Failed to resolve agent contexts for execution', { - error: toError(error).message, - }) - return [] - }), - ]) + const userPermission = workspaceAccess.permission + const [workspaceContext, integrationTools, mothershipTools, entitlements, agentContexts] = + await Promise.all([ + generateWorkspaceContext(workspaceId, userId, { workspaceAccess }), + buildIntegrationToolSchemas(userId, messageId, undefined, workspaceId), + Promise.all([ + buildSelectedMcpToolSchemas(userId, workspaceId, mcpTools ?? []), + buildTaggedMcpToolSchemas(userId, workspaceId, taggedMcpServerIds), + ]).then((groups) => { + const byName = new Map(groups.flat().map((tool) => [tool.name, tool])) + return [...byName.values()] + }), + computeWorkspaceEntitlements(workspaceId, userId), + processContextsServer( + nonMcpAgentMentions, + userId, + lastUserMessage, + workspaceId, + effectiveChatId + ).catch((error) => { + reqLogger.warn('Failed to resolve agent contexts for execution', { + error: toError(error).message, + }) + return [] + }), + ]) const requestPayload: Record = { messages, responseFormat, diff --git a/apps/sim/app/api/skills/[id]/members/route.ts b/apps/sim/app/api/skills/[id]/members/route.ts new file mode 100644 index 0000000000..27920ac9a7 --- /dev/null +++ b/apps/sim/app/api/skills/[id]/members/route.ts @@ -0,0 +1,251 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' +import { db } from '@sim/db' +import { skillMember } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { generateId } from '@sim/utils/id' +import { and, eq } from 'drizzle-orm' +import { type NextRequest, NextResponse } from 'next/server' +import { removeSkillMemberContract, upsertSkillMemberContract } from '@/lib/api/contracts/skills' +import { parseRequest } from '@/lib/api/server' +import { getSession } from '@/lib/auth' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { captureServerEvent } from '@/lib/posthog/server' +import { getSkillActorContext, listSkillEditors } from '@/lib/skills/access' +import { isBuiltinSkillId } from '@/lib/workflows/skills/builtin-skills' +import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' + +const logger = createLogger('SkillMembersAPI') + +interface RouteContext { + params: Promise<{ id: string }> +} + +type SkillEditorGate = + | { ok: true; workspaceId: string } + | { ok: false; reason: 'not-found' | 'not-editor' } + +/** + * Resolves the skill and asserts the actor can edit it (explicit editor row or + * derived workspace admin). Skills the actor cannot reach at all (missing, + * builtin, no workspace, no workspace access) read as not-found; + * visible-but-not-editor reads as forbidden. + */ +async function requireSkillEditor(skillId: string, userId: string): Promise { + if (isBuiltinSkillId(skillId)) return { ok: false, reason: 'not-found' } + + const actor = await getSkillActorContext(skillId, userId) + if (!actor.skill?.workspaceId || !actor.hasWorkspaceAccess) { + return { ok: false, reason: 'not-found' } + } + if (!actor.canEdit) return { ok: false, reason: 'not-editor' } + + return { ok: true, workspaceId: actor.skill.workspaceId } +} + +function skillEditorGateResponse(reason: 'not-found' | 'not-editor'): NextResponse { + return reason === 'not-found' + ? NextResponse.json({ error: 'Not found' }, { status: 404 }) + : NextResponse.json({ error: 'Skill editor access required' }, { status: 403 }) +} + +export const GET = withRouteHandler(async (_request: NextRequest, context: RouteContext) => { + try { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const { id: skillId } = await context.params + + if (isBuiltinSkillId(skillId)) { + return NextResponse.json({ error: 'Not found' }, { status: 404 }) + } + + const actor = await getSkillActorContext(skillId, session.user.id) + if (!actor.skill?.workspaceId || !actor.hasWorkspaceAccess) { + return NextResponse.json({ error: 'Not found' }, { status: 404 }) + } + + const editors = await listSkillEditors({ + id: actor.skill.id, + workspaceId: actor.skill.workspaceId, + }) + + return NextResponse.json({ editors }) + } catch (error) { + logger.error('Failed to fetch skill editors', { error }) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +}) + +export const POST = withRouteHandler(async (request: NextRequest, context: RouteContext) => { + try { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const { id: skillId } = await context.params + + const gate = await requireSkillEditor(skillId, session.user.id) + if (!gate.ok) { + logger.warn('Skill editor add denied', { + skillId, + actorId: session.user.id, + reason: gate.reason, + }) + return skillEditorGateResponse(gate.reason) + } + + const parsed = await parseRequest(upsertSkillMemberContract, request, context) + if (!parsed.success) return parsed.response + + const { userId } = parsed.data.body + + const targetWorkspacePerm = await getUserEntityPermissions( + userId, + 'workspace', + gate.workspaceId + ) + if (targetWorkspacePerm === null) { + return NextResponse.json({ error: 'User is not a member of this workspace' }, { status: 400 }) + } + if (targetWorkspacePerm === 'admin') { + return NextResponse.json( + { error: 'Workspace admins can always edit skills' }, + { status: 400 } + ) + } + + const [existing] = await db + .select({ id: skillMember.id }) + .from(skillMember) + .where(and(eq(skillMember.skillId, skillId), eq(skillMember.userId, userId))) + .limit(1) + + if (existing) { + return NextResponse.json({ success: true }) + } + + const now = new Date() + // Conflict-safe against a concurrent add racing the unique (skillId, userId) index. + const [inserted] = await db + .insert(skillMember) + .values({ + id: generateId(), + skillId, + userId, + invitedBy: session.user.id, + createdAt: now, + updatedAt: now, + }) + .onConflictDoNothing({ target: [skillMember.skillId, skillMember.userId] }) + .returning({ id: skillMember.id }) + + // A concurrent request won the race and created the row. The editor exists, + // so this is still a success — but this request added nothing, and emitting + // the share event or audit entry here would record an add that never happened. + if (!inserted) { + return NextResponse.json({ success: true }) + } + + captureServerEvent( + session.user.id, + 'skill_shared', + { skill_id: skillId, workspace_id: gate.workspaceId }, + { groups: { workspace: gate.workspaceId } } + ) + + recordAudit({ + workspaceId: gate.workspaceId, + actorId: session.user.id, + actorName: session.user.name, + actorEmail: session.user.email, + action: AuditAction.SKILL_MEMBER_ADDED, + resourceType: AuditResourceType.SKILL, + resourceId: skillId, + description: 'Added skill editor', + metadata: { targetUserId: userId }, + request, + }) + + return NextResponse.json({ success: true }, { status: 201 }) + } catch (error) { + logger.error('Failed to add skill editor', { error }) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +}) + +export const DELETE = withRouteHandler(async (request: NextRequest, context: RouteContext) => { + try { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const { id: skillId } = await context.params + + const gate = await requireSkillEditor(skillId, session.user.id) + if (!gate.ok) { + logger.warn('Skill editor removal denied', { + skillId, + actorId: session.user.id, + reason: gate.reason, + }) + return skillEditorGateResponse(gate.reason) + } + + const parsed = await parseRequest(removeSkillMemberContract, request, context) + if (!parsed.success) return parsed.response + + const { userId: targetUserId } = parsed.data.query + + const targetWorkspacePerm = await getUserEntityPermissions( + targetUserId, + 'workspace', + gate.workspaceId + ) + if (targetWorkspacePerm === 'admin') { + return NextResponse.json( + { error: 'Workspace admins can always edit skills' }, + { status: 400 } + ) + } + + // Hard delete — no deny markers and no last-editor guard: workspace admins + // always remain derived editors, so a skill can never be orphaned. + const removed = await db + .delete(skillMember) + .where(and(eq(skillMember.skillId, skillId), eq(skillMember.userId, targetUserId))) + .returning({ id: skillMember.id }) + + if (removed.length === 0) { + return NextResponse.json({ error: 'Editor not found' }, { status: 404 }) + } + + captureServerEvent( + session.user.id, + 'skill_unshared', + { skill_id: skillId, workspace_id: gate.workspaceId }, + { groups: { workspace: gate.workspaceId } } + ) + + recordAudit({ + workspaceId: gate.workspaceId, + actorId: session.user.id, + actorName: session.user.name, + actorEmail: session.user.email, + action: AuditAction.SKILL_MEMBER_REMOVED, + resourceType: AuditResourceType.SKILL, + resourceId: skillId, + description: 'Removed skill editor', + metadata: { targetUserId }, + request, + }) + + return NextResponse.json({ success: true }) + } catch (error) { + logger.error('Failed to remove skill editor', { error }) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +}) diff --git a/apps/sim/app/api/skills/import/route.ts b/apps/sim/app/api/skills/import/route.ts deleted file mode 100644 index 571f476407..0000000000 --- a/apps/sim/app/api/skills/import/route.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { createLogger } from '@sim/logger' -import { getErrorMessage } from '@sim/utils/errors' -import { type NextRequest, NextResponse } from 'next/server' -import { importSkillContract } from '@/lib/api/contracts' -import { parseRequest } from '@/lib/api/server' -import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' -import { generateRequestId } from '@/lib/core/utils/request' -import { withRouteHandler } from '@/lib/core/utils/with-route-handler' - -const logger = createLogger('SkillsImportAPI') - -const FETCH_TIMEOUT_MS = 15_000 - -/** - * Converts a standard GitHub file URL to its raw.githubusercontent.com equivalent. - * - * Supported formats: - * github.com/{owner}/{repo}/blob/{branch}/{path} - * raw.githubusercontent.com/{owner}/{repo}/{branch}/{path} (passthrough) - */ -function toRawGitHubUrl(url: string): string { - const parsed = new URL(url) - - if (parsed.hostname === 'raw.githubusercontent.com') { - return url - } - - if (parsed.hostname !== 'github.com') { - throw new Error('Only GitHub URLs are supported') - } - - const segments = parsed.pathname.split('/').filter(Boolean) - if (segments.length < 5 || segments[2] !== 'blob') { - throw new Error( - 'Invalid GitHub URL format. Expected: https://github.com/{owner}/{repo}/blob/{branch}/{path}' - ) - } - - const [owner, repo, , branch, ...pathParts] = segments - return `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${pathParts.join('/')}` -} - -/** POST - Fetch a SKILL.md from a GitHub URL and return its raw content */ -export const POST = withRouteHandler(async (req: NextRequest) => { - const requestId = generateRequestId() - - try { - const authResult = await checkSessionOrInternalAuth(req, { requireWorkflowId: false }) - if (!authResult.success || !authResult.userId) { - logger.warn(`[${requestId}] Unauthorized skill import attempt`) - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) - } - - const validation = await parseRequest(importSkillContract, req, {}) - if (!validation.success) return validation.response - const { url } = validation.data.body - - let rawUrl: string - try { - rawUrl = toRawGitHubUrl(url) - } catch (err) { - const message = getErrorMessage(err, 'Invalid URL') - return NextResponse.json({ error: message }, { status: 400 }) - } - - const response = await fetch(rawUrl, { - signal: AbortSignal.timeout(FETCH_TIMEOUT_MS), - headers: { Accept: 'text/plain' }, - }) - - if (!response.ok) { - logger.warn(`[${requestId}] GitHub fetch failed`, { - status: response.status, - url: rawUrl, - }) - return NextResponse.json( - { error: `Failed to fetch file (HTTP ${response.status}). Is the repository public?` }, - { status: 502 } - ) - } - - const contentLength = response.headers.get('content-length') - if (contentLength && Number.parseInt(contentLength, 10) > 100_000) { - return NextResponse.json({ error: 'File is too large (max 100KB)' }, { status: 400 }) - } - - const content = await response.text() - - if (content.length > 100_000) { - return NextResponse.json({ error: 'File is too large (max 100KB)' }, { status: 400 }) - } - - return NextResponse.json({ content }) - } catch (error) { - if (error instanceof Error && (error.name === 'AbortError' || error.name === 'TimeoutError')) { - logger.warn(`[${requestId}] GitHub fetch timed out`) - return NextResponse.json({ error: 'Request timed out' }, { status: 504 }) - } - - logger.error(`[${requestId}] Error importing skill`, error) - return NextResponse.json({ error: 'Failed to import skill' }, { status: 500 }) - } -}) diff --git a/apps/sim/app/api/skills/route.ts b/apps/sim/app/api/skills/route.ts index 5a945101be..f31f0881e7 100644 --- a/apps/sim/app/api/skills/route.ts +++ b/apps/sim/app/api/skills/route.ts @@ -11,9 +11,10 @@ import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { captureServerEvent } from '@/lib/posthog/server' +import { checkSkillsUpdateAccess, getSkillActorContext } from '@/lib/skills/access' import { isBuiltinSkillId } from '@/lib/workflows/skills/builtin-skills' -import { deleteSkill, listSkills, upsertSkills } from '@/lib/workflows/skills/operations' -import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' +import { deleteSkill, listSkillsForUser, upsertSkills } from '@/lib/workflows/skills/operations' +import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils' const logger = createLogger('SkillsAPI') @@ -41,13 +42,13 @@ export const GET = withRouteHandler(async (request: NextRequest) => { } const { workspaceId } = query.data - const userPermission = await getUserEntityPermissions(userId, 'workspace', workspaceId) - if (!userPermission) { + const workspaceAccess = await checkWorkspaceAccess(workspaceId, userId) + if (!workspaceAccess.hasAccess) { logger.warn(`[${requestId}] User ${userId} does not have access to workspace ${workspaceId}`) return NextResponse.json({ error: 'Access denied' }, { status: 403 }) } - const result = await listSkills({ workspaceId }) + const result = await listSkillsForUser({ workspaceId, userId, workspaceAccess }) const data = result.map((s) => ({ ...s, readOnly: isBuiltinSkillId(s.id) })) return NextResponse.json({ data }, { status: 200 }) @@ -85,8 +86,40 @@ export const POST = withRouteHandler(async (req: NextRequest) => { const { skills, workspaceId, source } = parsed.data.body - const userPermission = await getUserEntityPermissions(userId, 'workspace', workspaceId) - if (!userPermission || (userPermission !== 'admin' && userPermission !== 'write')) { + const workspaceAccess = await checkWorkspaceAccess(workspaceId, userId) + if (!workspaceAccess.hasAccess) { + logger.warn(`[${requestId}] User ${userId} does not have access to workspace ${workspaceId}`) + return NextResponse.json({ error: 'Access denied' }, { status: 403 }) + } + + if (skills.some((s) => s.id && isBuiltinSkillId(s.id))) { + return NextResponse.json({ error: 'Built-in skills are read-only' }, { status: 400 }) + } + + // Updating an existing skill requires editor access (explicit editor row + // or derived workspace admin); creating a new one requires workspace write. + const requestedIds = skills.flatMap((s) => (s.id ? [s.id] : [])) + const { existingIds, denied } = await checkSkillsUpdateAccess({ + workspaceId, + userId, + skillIds: requestedIds, + workspaceAccess, + }) + + if (denied.length > 0) { + logger.warn(`[${requestId}] User ${userId} is not an editor of skills being updated`, { + deniedSkillIds: denied.map((s) => s.id), + }) + return NextResponse.json( + { + error: `Skill editor access required to update: ${denied.map((s) => s.name).join(', ')}`, + }, + { status: 403 } + ) + } + + const hasCreates = skills.some((s) => !s.id || !existingIds.has(s.id)) + if (hasCreates && !workspaceAccess.canWrite) { logger.warn( `[${requestId}] User ${userId} does not have write permission for workspace ${workspaceId}` ) @@ -94,11 +127,12 @@ export const POST = withRouteHandler(async (req: NextRequest) => { } try { - const { skills: resultSkills, touched } = await upsertSkills({ + const { touched } = await upsertSkills({ skills, workspaceId, userId, requestId, + returnSkills: false, }) for (const { id, name, operation } of touched) { @@ -123,11 +157,17 @@ export const POST = withRouteHandler(async (req: NextRequest) => { ) } - return NextResponse.json({ success: true, data: resultSkills }) + const resultSkills = await listSkillsForUser({ workspaceId, userId, workspaceAccess }) + const data = resultSkills.map((s) => ({ ...s, readOnly: isBuiltinSkillId(s.id) })) + + return NextResponse.json({ success: true, data }) } catch (upsertError) { - if (upsertError instanceof Error && upsertError.message.includes('already exists')) { + if (upsertError instanceof Error && upsertError.message.includes('is unavailable')) { return NextResponse.json({ error: upsertError.message }, { status: 409 }) } + if (upsertError instanceof Error && upsertError.message.startsWith('Skill not found')) { + return NextResponse.json({ error: 'Skill not found' }, { status: 404 }) + } throw upsertError } } catch (error) { @@ -160,12 +200,16 @@ export const DELETE = withRouteHandler(async (request: NextRequest) => { } const { id: skillId, workspaceId, source } = query.data - const userPermission = await getUserEntityPermissions(userId, 'workspace', workspaceId) - if (!userPermission || (userPermission !== 'admin' && userPermission !== 'write')) { - logger.warn( - `[${requestId}] User ${userId} does not have write permission for workspace ${workspaceId}` - ) - return NextResponse.json({ error: 'Write permission required' }, { status: 403 }) + if (!isBuiltinSkillId(skillId)) { + const actor = await getSkillActorContext(skillId, userId) + if (!actor.skill || actor.skill.workspaceId !== workspaceId || !actor.hasWorkspaceAccess) { + logger.warn(`[${requestId}] Skill not found: ${skillId}`) + return NextResponse.json({ error: 'Skill not found' }, { status: 404 }) + } + if (!actor.canEdit) { + logger.warn(`[${requestId}] User ${userId} is not an editor of skill ${skillId}`) + return NextResponse.json({ error: 'Skill editor access required' }, { status: 403 }) + } } const deleted = await deleteSkill({ skillId, workspaceId }) diff --git a/apps/sim/app/api/v1/admin/workspaces/[id]/members/[memberId]/route.ts b/apps/sim/app/api/v1/admin/workspaces/[id]/members/[memberId]/route.ts index fc3082427a..bcc8df9dcf 100644 --- a/apps/sim/app/api/v1/admin/workspaces/[id]/members/[memberId]/route.ts +++ b/apps/sim/app/api/v1/admin/workspaces/[id]/members/[memberId]/route.ts @@ -34,6 +34,7 @@ import { import { parseRequest } from '@/lib/api/server' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { revokeWorkspaceCredentialMembershipsTx } from '@/lib/credentials/access' +import { removeWorkspaceSkillMembershipsTx } from '@/lib/skills/access' import { getWorkspaceById } from '@/lib/workspaces/permissions/utils' import { reassignWorkflowOwnershipForWorkspaceMemberRemovalTx, @@ -286,6 +287,7 @@ export const DELETE = withRouteHandler( await tx.delete(permissions).where(eq(permissions.id, memberId)) await revokeWorkspaceCredentialMembershipsTx(tx, workspaceId, existingMember.userId) + await removeWorkspaceSkillMembershipsTx(tx, workspaceId, existingMember.userId) }) logger.info(`Admin API: Removed member ${memberId} from workspace ${workspaceId}`, { diff --git a/apps/sim/app/api/v1/admin/workspaces/[id]/members/route.ts b/apps/sim/app/api/v1/admin/workspaces/[id]/members/route.ts index 0be8954b69..85523cd6c1 100644 --- a/apps/sim/app/api/v1/admin/workspaces/[id]/members/route.ts +++ b/apps/sim/app/api/v1/admin/workspaces/[id]/members/route.ts @@ -45,6 +45,7 @@ import { parseRequest } from '@/lib/api/server' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { revokeWorkspaceCredentialMembershipsTx } from '@/lib/credentials/access' import { syncWorkspaceEnvCredentials } from '@/lib/credentials/environment' +import { removeWorkspaceSkillMembershipsTx } from '@/lib/skills/access' import { getWorkspaceById } from '@/lib/workspaces/permissions/utils' import { reassignWorkflowOwnershipForWorkspaceMemberRemovalTx, @@ -371,6 +372,7 @@ export const DELETE = withRouteHandler( await tx.delete(permissions).where(eq(permissions.id, existingPermission.id)) await revokeWorkspaceCredentialMembershipsTx(tx, workspaceId, userId) + await removeWorkspaceSkillMembershipsTx(tx, workspaceId, userId) }) logger.info(`Admin API: Removed user ${userId} from workspace ${workspaceId}`) diff --git a/apps/sim/app/api/workspaces/members/[id]/route.ts b/apps/sim/app/api/workspaces/members/[id]/route.ts index 51a1e038c6..5472850c5e 100644 --- a/apps/sim/app/api/workspaces/members/[id]/route.ts +++ b/apps/sim/app/api/workspaces/members/[id]/route.ts @@ -12,6 +12,7 @@ import { reconcileOrganizationSeats } from '@/lib/billing/organizations/seats' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { revokeWorkspaceCredentialMembershipsTx } from '@/lib/credentials/access' import { captureServerEvent } from '@/lib/posthog/server' +import { removeWorkspaceSkillMembershipsTx } from '@/lib/skills/access' import { hasWorkspaceAdminAccess } from '@/lib/workspaces/permissions/utils' import { reassignWorkflowOwnershipForWorkspaceMemberRemovalTx, @@ -144,6 +145,7 @@ export const DELETE = withRouteHandler( ) await revokeWorkspaceCredentialMembershipsTx(tx, workspaceId, userId) + await removeWorkspaceSkillMembershipsTx(tx, workspaceId, userId) return { ownershipTransferred: didTransferOwnership, workflowOwnershipReassignment } } diff --git a/apps/sim/app/workspace/[workspaceId]/components/credential-detail/components/add-people-modal.tsx b/apps/sim/app/workspace/[workspaceId]/components/credential-detail/components/add-people-modal.tsx index f66a45b233..0a7a331deb 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/credential-detail/components/add-people-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/credential-detail/components/add-people-modal.tsx @@ -1,26 +1,14 @@ 'use client' - -import { useCallback, useMemo, useState } from 'react' +import { useCallback, useMemo } from 'react' import { - ChipModal, - ChipModalBody, - ChipModalError, - ChipModalField, - ChipModalFooter, - ChipModalHeader, -} from '@sim/emcn' -import { createLogger } from '@sim/logger' -import { getErrorMessage } from '@sim/utils/errors' -import { useWorkspacePermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' + type AddPeopleTarget, + type MemberRole, + AddPeopleModal as SharedAddPeopleModal, +} from '@/components/permissions' import { useUpsertWorkspaceCredentialMember, useWorkspaceCredentialMembers, - type WorkspaceCredentialRole, } from '@/hooks/queries/credentials' -import { ROLE_OPTIONS } from '../roles' -import { partitionSettledFailures, resolveAddEmail } from '../sharing' - -const logger = createLogger('AddPeopleModal') interface AddPeopleModalProps { credentialId: string @@ -29,28 +17,12 @@ interface AddPeopleModalProps { } /** - * Shared "Add people" modal: grants existing workspace members access to a - * credential with a chosen role. Emails are validated against the workspace - * roster and current membership; each add is an idempotent upsert and partial - * failures keep only the people that still need adding. + * "Add people" for a credential: wires the shared modal to credential + * membership. Active members count as already having access. */ export function AddPeopleModal({ credentialId, open, onOpenChange }: AddPeopleModalProps) { - const { workspacePermissions } = useWorkspacePermissionsContext() const { data: members = [] } = useWorkspaceCredentialMembers(credentialId) - const upsertMember = useUpsertWorkspaceCredentialMember() - - const [emailsToAdd, setEmailsToAdd] = useState([]) - const [roleToAdd, setRoleToAdd] = useState('member') - const [isAdding, setIsAdding] = useState(false) - const [submitError, setSubmitError] = useState(null) - - const workspaceUserIdByEmail = useMemo( - () => - new Map( - (workspacePermissions?.users ?? []).map((user) => [user.email.toLowerCase(), user.userId]) - ), - [workspacePermissions?.users] - ) + const { mutateAsync: upsertMemberAsync } = useUpsertWorkspaceCredentialMember() const existingMemberEmails = useMemo( () => @@ -63,108 +35,18 @@ export function AddPeopleModal({ credentialId, open, onOpenChange }: AddPeopleMo [members] ) - const validateAddEmail = useCallback( - (email: string): string | null => { - const result = resolveAddEmail(email, { workspaceUserIdByEmail, existingMemberEmails }) - return 'error' in result ? result.error : null - }, - [workspaceUserIdByEmail, existingMemberEmails] + const addMember = useCallback( + (target: AddPeopleTarget, role: MemberRole) => + upsertMemberAsync({ credentialId, userId: target.userId, role }), + [upsertMemberAsync, credentialId] ) - const handleClose = useCallback(() => { - setEmailsToAdd([]) - setRoleToAdd('member') - setSubmitError(null) - onOpenChange(false) - }, [onOpenChange]) - - const handleAddPeople = useCallback(async () => { - if (emailsToAdd.length === 0 || isAdding) return - setSubmitError(null) - const targets = emailsToAdd - .map((email) => { - const result = resolveAddEmail(email, { workspaceUserIdByEmail, existingMemberEmails }) - return 'userId' in result ? { email, userId: result.userId } : null - }) - .filter((target): target is { email: string; userId: string } => target !== null) - if (targets.length === 0) return - - setIsAdding(true) - try { - const results = await Promise.allSettled( - targets.map((target) => - upsertMember.mutateAsync({ credentialId, userId: target.userId, role: roleToAdd }) - ) - ) - const failures = partitionSettledFailures(targets, results) - if (failures.length === 0) { - handleClose() - return - } - setEmailsToAdd(failures.map((target) => target.email)) - const firstError = results.find( - (result): result is PromiseRejectedResult => result.status === 'rejected' - ) - logger.error('Failed to add some credential members', firstError?.reason) - const reason = getErrorMessage(firstError?.reason, 'Please try again in a moment.') - setSubmitError( - failures.length === targets.length - ? `Couldn't add people. ${reason}` - : `Couldn't add ${failures.length} of ${targets.length} people. ${reason}` - ) - } finally { - setIsAdding(false) - } - }, [ - credentialId, - emailsToAdd, - isAdding, - workspaceUserIdByEmail, - existingMemberEmails, - roleToAdd, - upsertMember, - handleClose, - ]) - return ( - { - if (!next) handleClose() - }} - srTitle='Add people' - > - Add people - - - setRoleToAdd(role as WorkspaceCredentialRole)} - disabled={isAdding} - /> - {submitError} - - - + onOpenChange={onOpenChange} + existingMemberEmails={existingMemberEmails} + addMember={addMember} + /> ) } diff --git a/apps/sim/app/workspace/[workspaceId]/components/credential-detail/components/credential-members-section.tsx b/apps/sim/app/workspace/[workspaceId]/components/credential-detail/components/credential-members-section.tsx index 2d8a0e9c54..b08907f2cc 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/credential-detail/components/credential-members-section.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/credential-detail/components/credential-members-section.tsx @@ -1,15 +1,16 @@ 'use client' -import { Avatar, AvatarFallback, Chip, ChipDropdown, cn } from '@sim/emcn' import { createLogger } from '@sim/logger' -import { credentialRoleLockReason, RoleLockTooltip } from '@/components/permissions' -import { getUserColor } from '@/lib/workspaces/colors' +import { + credentialRoleLockReason, + MEMBER_ROLE_OPTIONS, + type MemberRole, + MemberRow, +} from '@/components/permissions' import { useRemoveWorkspaceCredentialMember, useUpsertWorkspaceCredentialMember, useWorkspaceCredentialMembers, - type WorkspaceCredentialRole, } from '@/hooks/queries/credentials' -import { ROLE_OPTIONS } from '../roles' import { DetailSection } from './detail-section' const logger = createLogger('CredentialMembersSection') @@ -35,7 +36,7 @@ export function CredentialMembersSection({ credentialId, isAdmin }: CredentialMe (member) => member.role === 'admin' && member.roleSource !== 'workspace-admin' ).length - const handleChangeMemberRole = async (userId: string, role: WorkspaceCredentialRole) => { + const handleChangeMemberRole = async (userId: string, role: MemberRole) => { const current = activeMembers.find((member) => member.userId === userId) if (current?.role === role) return try { @@ -63,58 +64,18 @@ export function CredentialMembersSection({ credentialId, isAdmin }: CredentialMe member.role === 'admin' && member.roleSource !== 'workspace-admin' && explicitAdminCount <= 1 - const roleDisabled = !isAdmin || roleLocked || lockReason !== null - const removeDisabled = roleLocked || lockReason !== null return ( -
-
- - - {(member.userName || member.userEmail || '?').charAt(0).toUpperCase()} - - -
- - {member.userName || member.userEmail || member.userId} - - - {member.userEmail || member.userId} - -
-
- - - handleChangeMemberRole(member.userId, role as WorkspaceCredentialRole) - } - /> - - {isAdmin && ( - handleRemoveMember(member.userId)} - disabled={removeDisabled} - flush - className='justify-self-end' - > - Remove - - )} -
+ member={member} + roleOptions={MEMBER_ROLE_OPTIONS} + lockReason={lockReason} + canManage={isAdmin} + roleDisabled={!isAdmin || roleLocked || lockReason !== null} + removeDisabled={roleLocked || lockReason !== null} + onRoleChange={(role) => handleChangeMemberRole(member.userId, role)} + onRemove={() => handleRemoveMember(member.userId)} + /> ) })} diff --git a/apps/sim/app/workspace/[workspaceId]/components/credential-detail/roles.ts b/apps/sim/app/workspace/[workspaceId]/components/credential-detail/roles.ts deleted file mode 100644 index 2956f8ed07..0000000000 --- a/apps/sim/app/workspace/[workspaceId]/components/credential-detail/roles.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { WorkspaceCredentialRole } from '@/hooks/queries/credentials' - -export interface CredentialRoleOption { - value: WorkspaceCredentialRole - label: string -} - -/** - * Roles assignable to a credential member. Shared by every credential detail - * surface (Integrations, Secrets) so role choices never drift between them. - */ -export const ROLE_OPTIONS: readonly CredentialRoleOption[] = [ - { value: 'member', label: 'Member' }, - { value: 'admin', label: 'Admin' }, -] as const diff --git a/apps/sim/app/workspace/[workspaceId]/components/custom-tool-editor/custom-tool-code-field.tsx b/apps/sim/app/workspace/[workspaceId]/components/custom-tool-editor/custom-tool-code-field.tsx new file mode 100644 index 0000000000..9290ca41b2 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/components/custom-tool-editor/custom-tool-code-field.tsx @@ -0,0 +1,380 @@ +'use client' + +import { useEffect, useRef, useState } from 'react' +import { + ChipTag, + CODE_LINE_HEIGHT_PX, + Popover, + PopoverAnchor, + PopoverContent, + PopoverItem, + PopoverScrollArea, + PopoverSection, +} from '@sim/emcn' +import { createLogger } from '@sim/logger' +import { + CODE_PLACEHOLDER, + type SchemaParameter, +} from '@/app/workspace/[workspaceId]/components/custom-tool-editor/custom-tool-schema' +import type { useCodeGeneration } from '@/app/workspace/[workspaceId]/components/custom-tool-editor/use-custom-tool-generation' +import { + checkEnvVarTrigger, + EnvVarDropdown, +} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/env-var-dropdown' +import { + checkTagTrigger, + TagDropdown, +} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown' +import { CodeEditor } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/code-editor/code-editor' + +const logger = createLogger('CustomToolCodeField') + +interface CustomToolCodeFieldProps { + value: string + onChange: (value: string) => void + error: boolean + generation: ReturnType + schemaParameters: SchemaParameter[] + workspaceId: string + /** + * Workflow block the editor is embedded in. Only present on the canvas — + * without it there is no upstream block output to reference, so the `<` + * tag autocomplete is not offered. + */ + blockId?: string + /** Renders the editor inert for viewers without edit rights. */ + disabled?: boolean +} + +interface TriggerState { + show: boolean + searchTerm: string +} + +/** Trailing identifier under the caret — the unit both the trigger and the completion act on. */ +const SCHEMA_PARAM_WORD = /[a-zA-Z_]\w*$/ + +function checkSchemaParamTrigger( + text: string, + cursorPos: number, + parameters: SchemaParameter[] +): TriggerState { + if (parameters.length === 0) return { show: false, searchTerm: '' } + + const currentWord = text.slice(0, cursorPos).match(SCHEMA_PARAM_WORD)?.[0] ?? '' + if (!currentWord) return { show: false, searchTerm: '' } + + const lower = currentWord.toLowerCase() + const hasMatch = parameters.some((param) => param.name.toLowerCase().startsWith(lower)) + return { show: hasMatch, searchTerm: currentWord } +} + +/** + * The code half of the custom tool editor: the available-parameters strip, the + * JavaScript editor, and its three caret-anchored autocompletes (environment + * variables, upstream block tags, and schema parameters). The surrounding + * surface owns the section label, the "Generate" action, and the error message. + */ +export function CustomToolCodeField({ + value, + onChange, + error, + generation, + schemaParameters, + workspaceId, + blockId, + disabled = false, +}: CustomToolCodeFieldProps) { + const codeEditorRef = useRef(null) + const schemaParamItemRefs = useRef | null>(null) + schemaParamItemRefs.current ??= new Map() + + const [showEnvVars, setShowEnvVars] = useState(false) + const [showTags, setShowTags] = useState(false) + const [showSchemaParams, setShowSchemaParams] = useState(false) + const [searchTerm, setSearchTerm] = useState('') + const [cursorPosition, setCursorPosition] = useState(0) + const [activeSourceBlockId, setActiveSourceBlockId] = useState(null) + const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0 }) + const [schemaParamSelectedIndex, setSchemaParamSelectedIndex] = useState(0) + + const busy = disabled || generation.isLoading || generation.isStreaming + const resolvedMinHeight = schemaParameters.length > 0 ? '380px' : '420px' + + /** Generation writes bypass `handleChange`, so close any open menu here instead. */ + useEffect(() => { + if (!busy) return + setShowEnvVars(false) + setShowTags(false) + setShowSchemaParams(false) + }, [busy]) + + useEffect(() => { + if (!showSchemaParams || schemaParamSelectedIndex < 0) return + const element = schemaParamItemRefs.current?.get(schemaParamSelectedIndex) + element?.scrollIntoView({ behavior: 'smooth', block: 'nearest' }) + }, [schemaParamSelectedIndex, showSchemaParams]) + + const handleChange = (newValue: string) => { + onChange(newValue) + if (busy) return + + const container = codeEditorRef.current + const textarea = container?.querySelector('textarea') + if (!container || !textarea) return + + const pos = textarea.selectionStart + setCursorPosition(pos) + + const textBeforeCursor = newValue.substring(0, pos) + const lines = textBeforeCursor.split('\n') + const currentLine = lines.length + const currentCol = lines[lines.length - 1].length + + const editorRect = container.getBoundingClientRect() + setDropdownPosition({ + top: currentLine * CODE_LINE_HEIGHT_PX + 5, + left: Math.min(currentCol * 8, editorRect.width - 260), + }) + + const envVarTrigger = checkEnvVarTrigger(newValue, pos) + setShowEnvVars(envVarTrigger.show) + setSearchTerm(envVarTrigger.show ? envVarTrigger.searchTerm : '') + + if (blockId) { + const tagTrigger = checkTagTrigger(newValue, pos) + setShowTags(tagTrigger.show) + if (!tagTrigger.show) setActiveSourceBlockId(null) + } + + if (schemaParameters.length > 0) { + const schemaParamTrigger = checkSchemaParamTrigger(newValue, pos, schemaParameters) + if (schemaParamTrigger.show && !showSchemaParams) { + setShowSchemaParams(true) + setSchemaParamSelectedIndex(0) + } else if (!schemaParamTrigger.show && showSchemaParams) { + setShowSchemaParams(false) + } + } + } + + const handleSchemaParamSelect = (paramName: string) => { + const textarea = codeEditorRef.current?.querySelector('textarea') + if (!textarea) return + + const pos = textarea.selectionStart + const beforeCursor = value.substring(0, pos) + const afterCursor = value.substring(pos) + + // Must match checkSchemaParamTrigger's boundary exactly — a looser split + // here would replace text the trigger never matched (e.g. eat `data.`). + const currentWord = beforeCursor.match(SCHEMA_PARAM_WORD)?.[0] ?? '' + const wordStart = pos - currentWord.length + + onChange(beforeCursor.substring(0, wordStart) + paramName + afterCursor) + setShowSchemaParams(false) + + requestAnimationFrame(() => { + textarea.focus() + const caret = wordStart + paramName.length + textarea.setSelectionRange(caret, caret) + }) + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Escape') { + if (showEnvVars || showTags || showSchemaParams) { + setShowEnvVars(false) + setShowTags(false) + setShowSchemaParams(false) + e.preventDefault() + e.stopPropagation() + return + } + } + + if (generation.isStreaming) { + e.preventDefault() + return + } + + if (showSchemaParams && schemaParameters.length > 0) { + switch (e.key) { + case 'ArrowDown': + e.preventDefault() + e.stopPropagation() + setSchemaParamSelectedIndex((prev) => Math.min(prev + 1, schemaParameters.length - 1)) + return + case 'ArrowUp': + e.preventDefault() + e.stopPropagation() + setSchemaParamSelectedIndex((prev) => Math.max(prev - 1, 0)) + return + case 'Enter': { + e.preventDefault() + e.stopPropagation() + const selectedParam = schemaParameters[schemaParamSelectedIndex] + if (selectedParam) handleSchemaParamSelect(selectedParam.name) + return + } + case 'Escape': + e.preventDefault() + e.stopPropagation() + setShowSchemaParams(false) + return + case ' ': + case 'Tab': + setShowSchemaParams(false) + return + } + } + + if (showEnvVars || showTags) { + if (['ArrowDown', 'ArrowUp', 'Enter'].includes(e.key)) { + e.preventDefault() + e.stopPropagation() + } + } + } + + return ( +
+ {schemaParameters.length > 0 && ( +
+ Available parameters: + {schemaParameters.map((param) => ( + + {param.name} + + ))} + + Start typing a parameter name for autocomplete. + +
+ )} + +
+ + + {showEnvVars && ( + { + onChange(newValue) + setShowEnvVars(false) + }} + searchTerm={searchTerm} + inputValue={value} + cursorPosition={cursorPosition} + workspaceId={workspaceId} + onClose={() => { + setShowEnvVars(false) + setSearchTerm('') + }} + className='w-64' + style={{ + position: 'absolute', + top: `${dropdownPosition.top}px`, + left: `${dropdownPosition.left}px`, + }} + /> + )} + + {showTags && blockId && ( + { + onChange(newValue) + setShowTags(false) + setActiveSourceBlockId(null) + }} + blockId={blockId} + activeSourceBlockId={activeSourceBlockId} + inputValue={value} + cursorPosition={cursorPosition} + onClose={() => { + setShowTags(false) + setActiveSourceBlockId(null) + }} + className='w-64' + style={{ + position: 'absolute', + top: `${dropdownPosition.top}px`, + left: `${dropdownPosition.left}px`, + }} + /> + )} + + {showSchemaParams && schemaParameters.length > 0 && ( + { + if (!open) setShowSchemaParams(false) + }} + colorScheme='inverted' + > + +
+ + e.preventDefault()} + onCloseAutoFocus={(e) => e.preventDefault()} + > + + Available Parameters + {schemaParameters.map((param, index) => ( + setSchemaParamSelectedIndex(index)} + onMouseDown={(e) => { + e.preventDefault() + e.stopPropagation() + handleSchemaParamSelect(param.name) + }} + ref={(el) => { + if (el) schemaParamItemRefs.current?.set(index, el) + }} + > + {param.name} + {param.type && param.type !== 'any' && ( + + {param.type} + + )} + + ))} + + + + )} +
+
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/components/custom-tool-editor/custom-tool-schema-field.tsx b/apps/sim/app/workspace/[workspaceId]/components/custom-tool-editor/custom-tool-schema-field.tsx new file mode 100644 index 0000000000..2fb38059dd --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/components/custom-tool-editor/custom-tool-schema-field.tsx @@ -0,0 +1,41 @@ +'use client' + +import { SCHEMA_PLACEHOLDER } from '@/app/workspace/[workspaceId]/components/custom-tool-editor/custom-tool-schema' +import type { useSchemaGeneration } from '@/app/workspace/[workspaceId]/components/custom-tool-editor/use-custom-tool-generation' +import { CodeEditor } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/code-editor/code-editor' + +interface CustomToolSchemaFieldProps { + value: string + onChange: (value: string) => void + error: boolean + generation: ReturnType + /** Renders the editor inert for viewers without edit rights. */ + disabled?: boolean +} + +/** + * The JSON-schema half of the custom tool editor. The surrounding surface owns + * the section label, the "Generate" action, and the error message — this field + * is just the editor, so both consumers can frame it however they need. + */ +export function CustomToolSchemaField({ + value, + onChange, + error, + generation, + disabled = false, +}: CustomToolSchemaFieldProps) { + const busy = disabled || generation.isLoading || generation.isStreaming + + return ( + + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/components/custom-tool-editor/custom-tool-schema.ts b/apps/sim/app/workspace/[workspaceId]/components/custom-tool-editor/custom-tool-schema.ts new file mode 100644 index 0000000000..542709bf2d --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/components/custom-tool-editor/custom-tool-schema.ts @@ -0,0 +1,126 @@ +/** + * Shared parsing/validation for a custom tool's OpenAI function-calling JSON + * schema. Used by both custom-tool editing surfaces (the canvas modal and the + * Settings > Custom Tools detail page) so they agree on what a valid schema is. + */ + +export interface SchemaParameter { + name: string + type: string + description: string + required: boolean +} + +interface SchemaValidation { + isValid: boolean + error: string | null +} + +export const SCHEMA_PLACEHOLDER = `{ + "type": "function", + "function": { + "name": "addItemToOrder", + "description": "Add one quantity of a food item to the order.", + "parameters": { + "type": "object", + "properties": { + "itemName": { + "type": "string", + "description": "The name of the food item to add to order" + } + }, + "required": ["itemName"] + } + } +}` + +export const CODE_PLACEHOLDER = 'return schemaVariable + {{environmentVariable}}' + +/** Shown when the server rejects a rename — `function.name` is immutable after creation. */ +export const FUNCTION_NAME_LOCKED = + 'Function name cannot be changed after creation. To use a different name, delete this tool and create a new one.' + +/** Delete-confirmation copy, shared by both editing surfaces. */ +export const CUSTOM_TOOL_DELETE_CONFIRM_TEXT = [ + { + text: 'This will permanently delete the tool and remove it from any workflows that are using it.', + error: true, + }, + ' This action cannot be undone.', +] as const + +/** Validates the shape the executor and providers expect. */ +export function validateCustomToolSchema(schema: string): SchemaValidation { + if (!schema) return { isValid: false, error: null } + + try { + const parsed = JSON.parse(schema) + + if (!parsed.type || parsed.type !== 'function') { + return { isValid: false, error: 'Missing "type": "function"' } + } + if (!parsed.function || !parsed.function.name) { + return { isValid: false, error: 'Missing function.name field' } + } + if (!parsed.function.parameters) { + return { isValid: false, error: 'Missing function.parameters object' } + } + if (!parsed.function.parameters.type) { + return { isValid: false, error: 'Missing parameters.type field' } + } + if (parsed.function.parameters.properties === undefined) { + return { isValid: false, error: 'Missing parameters.properties field' } + } + if ( + typeof parsed.function.parameters.properties !== 'object' || + parsed.function.parameters.properties === null + ) { + return { isValid: false, error: 'parameters.properties must be an object' } + } + + return { isValid: true, error: null } + } catch { + return { isValid: false, error: 'Invalid JSON format' } + } +} + +/** + * The tool's identity as declared inside its schema. Name and description have + * no separate storage — `schema.function.name` IS the tool's title (the save + * path derives it), so surfaces read them back out rather than offering a second + * place to edit them. + */ +export function extractSchemaIdentity(jsonSchema: string): { + name: string | null + description: string | null +} { + try { + const fn = JSON.parse(jsonSchema)?.function + return { + name: typeof fn?.name === 'string' && fn.name ? fn.name : null, + description: typeof fn?.description === 'string' && fn.description ? fn.description : null, + } + } catch { + return { name: null, description: null } + } +} + +/** Flattens a schema's properties into the parameter list the code editor autocompletes against. */ +export function extractSchemaParameters(jsonSchema: string): SchemaParameter[] { + try { + if (!jsonSchema) return [] + const parsed = JSON.parse(jsonSchema) + const properties = parsed?.function?.parameters?.properties + if (!properties) return [] + + const required = new Set(parsed?.function?.parameters?.required ?? []) + return Object.keys(properties).map((key) => ({ + name: key, + type: properties[key].type || 'any', + description: properties[key].description || '', + required: required.has(key), + })) + } catch { + return [] + } +} diff --git a/apps/sim/app/workspace/[workspaceId]/components/custom-tool-editor/field-error-text.tsx b/apps/sim/app/workspace/[workspaceId]/components/custom-tool-editor/field-error-text.tsx new file mode 100644 index 0000000000..6f8bdddffa --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/components/custom-tool-editor/field-error-text.tsx @@ -0,0 +1,10 @@ +import type { ReactNode } from 'react' + +/** + * Inline error text for a custom-tool editor section header. Lives in the + * header rather than under the editor because a tall editor plus a message + * below it shifts everything after it as the message appears while typing. + */ +export function FieldErrorText({ children }: { children: ReactNode }) { + return {children} +} diff --git a/apps/sim/app/workspace/[workspaceId]/components/custom-tool-editor/generate-prompt-control.tsx b/apps/sim/app/workspace/[workspaceId]/components/custom-tool-editor/generate-prompt-control.tsx new file mode 100644 index 0000000000..a0d07c1e51 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/components/custom-tool-editor/generate-prompt-control.tsx @@ -0,0 +1,90 @@ +'use client' + +import { useRef, useState } from 'react' +import { Chip, ChipInput } from '@sim/emcn' +import { ArrowUp } from 'lucide-react' + +interface GeneratePromptControlProps { + isLoading: boolean + isStreaming: boolean + onSubmit: (prompt: string) => void +} + +/** + * The "Generate" affordance above a custom-tool editor: a chip that swaps into + * an inline prompt field, then hands the trimmed prompt to the caller's wand + * stream. Owns only its own transient input state so both the schema and code + * fields can reuse it. + */ +export function GeneratePromptControl({ + isLoading, + isStreaming, + onSubmit, +}: GeneratePromptControlProps) { + const [isActive, setIsActive] = useState(false) + const [prompt, setPrompt] = useState('') + const inputRef = useRef(null) + + const activate = () => { + if (isLoading || isStreaming) return + setIsActive(true) + setPrompt('') + requestAnimationFrame(() => inputRef.current?.focus()) + } + + const submit = () => { + const trimmed = prompt.trim() + if (!trimmed || isLoading || isStreaming) return + onSubmit(trimmed) + setPrompt('') + setIsActive(false) + } + + if (!isActive) { + return ( + + Generate + + ) + } + + return ( +
+ setPrompt(e.target.value)} + onBlur={() => { + if (!prompt.trim() && !isStreaming) setIsActive(false) + }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault() + submit() + } else if (e.key === 'Escape') { + e.preventDefault() + setPrompt('') + setIsActive(false) + } + }} + disabled={isStreaming} + className='w-[220px]' + placeholder='Describe what to generate...' + /> + { + e.preventDefault() + e.stopPropagation() + }} + onClick={(e) => { + e.stopPropagation() + submit() + }} + /> +
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/components/custom-tool-editor/index.ts b/apps/sim/app/workspace/[workspaceId]/components/custom-tool-editor/index.ts new file mode 100644 index 0000000000..8ab25cc5e0 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/components/custom-tool-editor/index.ts @@ -0,0 +1,15 @@ +export { CustomToolCodeField } from '@/app/workspace/[workspaceId]/components/custom-tool-editor/custom-tool-code-field' +export { + CUSTOM_TOOL_DELETE_CONFIRM_TEXT, + extractSchemaIdentity, + extractSchemaParameters, + FUNCTION_NAME_LOCKED, + validateCustomToolSchema, +} from '@/app/workspace/[workspaceId]/components/custom-tool-editor/custom-tool-schema' +export { CustomToolSchemaField } from '@/app/workspace/[workspaceId]/components/custom-tool-editor/custom-tool-schema-field' +export { FieldErrorText } from '@/app/workspace/[workspaceId]/components/custom-tool-editor/field-error-text' +export { GeneratePromptControl } from '@/app/workspace/[workspaceId]/components/custom-tool-editor/generate-prompt-control' +export { + useCodeGeneration, + useSchemaGeneration, +} from '@/app/workspace/[workspaceId]/components/custom-tool-editor/use-custom-tool-generation' diff --git a/apps/sim/app/workspace/[workspaceId]/components/custom-tool-editor/use-custom-tool-generation.ts b/apps/sim/app/workspace/[workspaceId]/components/custom-tool-editor/use-custom-tool-generation.ts new file mode 100644 index 0000000000..6bb9427943 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/components/custom-tool-editor/use-custom-tool-generation.ts @@ -0,0 +1,193 @@ +'use client' + +import { useMemo } from 'react' +import type { SchemaParameter } from '@/app/workspace/[workspaceId]/components/custom-tool-editor/custom-tool-schema' +import { useWand } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-wand' + +const SCHEMA_PROMPT = `You are an expert programmer specializing in creating OpenAI function calling format JSON schemas for custom tools. +Generate ONLY the JSON schema based on the user's request. +The output MUST be a single, valid JSON object, starting with { and ending with }. +The JSON schema MUST follow this specific format: +1. Top-level property "type" must be set to "function" +2. A "function" object containing: + - "name": A concise, camelCase name for the function + - "description": A clear description of what the function does + - "parameters": A JSON Schema object describing the function's parameters with: + - "type": "object" + - "properties": An object containing parameter definitions + - "required": An array of required parameter names + +Current schema: {context} + +Do not include any explanations, markdown formatting, or other text outside the JSON object. + +Valid Schema Examples: + +Example 1: +{ + "type": "function", + "function": { + "name": "getWeather", + "description": "Fetches the current weather for a specific location.", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state, e.g., San Francisco, CA" + }, + "unit": { + "type": "string", + "description": "Temperature unit", + "enum": ["celsius", "fahrenheit"] + } + }, + "required": ["location"], + "additionalProperties": false + } + } +} + +Example 2: +{ + "type": "function", + "function": { + "name": "addItemToOrder", + "description": "Add one quantity of a food item to the order.", + "parameters": { + "type": "object", + "properties": { + "itemName": { + "type": "string", + "description": "The name of the food item to add to order" + }, + "quantity": { + "type": "integer", + "description": "The quantity of the item to add", + "default": 1 + } + }, + "required": ["itemName"], + "additionalProperties": false + } + } +}` + +function buildCodePrompt(schemaContext: string): string { + return `You are an expert JavaScript programmer. +Generate ONLY the raw body of a JavaScript function based on the user's request. +The code should be executable within an 'async function(params, environmentVariables) {...}' context. +- 'params' (object): Contains input parameters derived from the JSON schema. Reference these directly by name (e.g., 'userId', 'cityName'). Do NOT use 'params.paramName'. +- 'environmentVariables' (object): Contains environment variables. Reference these using the double curly brace syntax: '{{ENV_VAR_NAME}}'. Do NOT use 'environmentVariables.VAR_NAME' or env. + +${schemaContext} + +Current code: {context} + +IMPORTANT FORMATTING RULES: +1. Reference Environment Variables: Use the exact syntax {{VARIABLE_NAME}}. Do NOT wrap it in quotes (e.g., use 'const apiKey = {{SERVICE_API_KEY}};' not 'const apiKey = "{{SERVICE_API_KEY}}";'). Our system replaces these placeholders before execution. +2. Reference Input Parameters/Workflow Variables: Reference them directly by name (e.g., 'const city = cityName;' or use directly in template strings like \`\${cityName}\`). Do NOT wrap in quotes or angle brackets. +3. Function Body ONLY: Do NOT include the function signature (e.g., 'async function myFunction() {' or the surrounding '}'). +4. Imports: Do NOT include import/require statements unless they are standard Node.js built-in modules (e.g., 'crypto', 'fs'). External libraries are not supported in this context. +5. Output: Ensure the code returns a value if the function is expected to produce output. Use 'return'. +6. Clarity: Write clean, readable code. +7. No Explanations: Do NOT include markdown formatting, comments explaining the rules, or any text other than the raw JavaScript code for the function body. + +Example Scenario: +User Prompt: "Fetch weather data from OpenWeather API. Use the city name passed in as 'cityName' and an API Key stored as the 'OPENWEATHER_API_KEY' environment variable." + +Generated Code: +const apiKey = {{OPENWEATHER_API_KEY}}; +const url = \`https://api.openweathermap.org/data/2.5/weather?q=\${cityName}&appid=\${apiKey}\`; + +try { + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }); + + if (!response.ok) { + throw new Error(\`API request failed with status \${response.status}: \${await response.text()}\`); + } + + const weatherData = await response.json(); + return weatherData; +} catch (error) { + console.error(\`Error fetching weather data: \${error.message}\`); + throw error; +}` +} + +interface UseSchemaGenerationParams { + jsonSchema: string + setJsonSchema: (updater: (prev: string) => string) => void + replaceJsonSchema: (value: string) => void +} + +/** Wand-driven generation for a custom tool's JSON schema. */ +export function useSchemaGeneration({ + jsonSchema, + setJsonSchema, + replaceJsonSchema, +}: UseSchemaGenerationParams) { + return useWand({ + wandConfig: { + enabled: true, + maintainHistory: true, + prompt: SCHEMA_PROMPT, + placeholder: 'Describe the function parameters and structure...', + generationType: 'custom-tool-schema', + }, + currentValue: jsonSchema, + onStreamStart: () => replaceJsonSchema(''), + onGeneratedContent: (content) => replaceJsonSchema(content), + onStreamChunk: (chunk) => setJsonSchema((prev) => prev + chunk), + }) +} + +interface UseCodeGenerationParams { + functionCode: string + schemaParameters: SchemaParameter[] + setFunctionCode: (updater: (prev: string) => string) => void + replaceFunctionCode: (value: string) => void +} + +/** Wand-driven generation for a custom tool's function body, aware of the schema's parameters. */ +export function useCodeGeneration({ + functionCode, + schemaParameters, + setFunctionCode, + replaceFunctionCode, +}: UseCodeGenerationParams) { + const prompt = useMemo(() => { + if (schemaParameters.length === 0) { + return buildCodePrompt( + 'Schema parameters: (none defined yet — the user has not added any parameters to the schema)' + ) + } + const lines = schemaParameters.map((p) => { + const requiredLabel = p.required ? 'required' : 'optional' + const description = p.description ? `: ${p.description}` : '' + return `- ${p.name} (${p.type}, ${requiredLabel})${description}` + }) + return buildCodePrompt( + `Schema parameters (reference these directly by name in the generated code):\n${lines.join('\n')}` + ) + }, [schemaParameters]) + + return useWand({ + wandConfig: { + enabled: true, + maintainHistory: true, + prompt, + placeholder: 'Describe the JavaScript function to generate...', + generationType: 'javascript-function-body', + }, + currentValue: functionCode, + onStreamStart: () => replaceFunctionCode(''), + onGeneratedContent: (content) => replaceFunctionCode(content), + onStreamChunk: (chunk) => setFunctionCode((prev) => prev + chunk), + }) +} diff --git a/apps/sim/app/workspace/[workspaceId]/components/index.ts b/apps/sim/app/workspace/[workspaceId]/components/index.ts index 4d5ef5a38d..02855214e2 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/components/index.ts @@ -36,4 +36,5 @@ export type { SelectableConfig, } from './resource/resource' export { EMPTY_CELL_PLACEHOLDER, Resource } from './resource/resource' +export { ResourceTile } from './resource-tile' export { SkillTile } from './skill-tile' diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource-tile/index.ts b/apps/sim/app/workspace/[workspaceId]/components/resource-tile/index.ts new file mode 100644 index 0000000000..507fe07a2f --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/components/resource-tile/index.ts @@ -0,0 +1 @@ +export { ResourceTile } from '@/app/workspace/[workspaceId]/components/resource-tile/resource-tile' diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource-tile/resource-tile.tsx b/apps/sim/app/workspace/[workspaceId]/components/resource-tile/resource-tile.tsx new file mode 100644 index 0000000000..9090700143 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/components/resource-tile/resource-tile.tsx @@ -0,0 +1,20 @@ +import type { ComponentType } from 'react' + +interface ResourceTileProps { + icon: ComponentType<{ className?: string }> +} + +/** + * Square glyph tile identifying a workspace resource — the leading visual on a + * resource's row and on its detail heading. Single source for that chrome so + * the skills and custom tools surfaces cannot drift apart. + */ +export function ResourceTile({ icon: Icon }: ResourceTileProps) { + return ( +
+
+ +
+
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/components/skill-tile/skill-tile.tsx b/apps/sim/app/workspace/[workspaceId]/components/skill-tile/skill-tile.tsx index 7839dac455..7ac1095a2c 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/skill-tile/skill-tile.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/skill-tile/skill-tile.tsx @@ -1,4 +1,5 @@ import { AgentSkillsIcon } from '@/components/icons' +import { ResourceTile } from '@/app/workspace/[workspaceId]/components/resource-tile' /** * Square tile bearing the agent-skills glyph. Shared chrome for any surface @@ -6,11 +7,5 @@ import { AgentSkillsIcon } from '@/components/icons' * do not drift. */ export function SkillTile() { - return ( -
-
- -
-
- ) + return } diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/mention/mention-chip.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/mention/mention-chip.tsx index b851c2a86f..4f261851c4 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/mention/mention-chip.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/mention/mention-chip.tsx @@ -36,8 +36,8 @@ export function MentionChipView({ node, editor }: ReactNodeViewProps) { const router = useRouter() const params = useParams() const { kind, id, label } = node.attrs as MentionAttrs - const Icon = mentionIcon(kind, id) as StyleableIcon - const iconStyle = getBareIconStyle(Icon) + const Icon = mentionIcon(kind, id, label) as StyleableIcon | undefined + const iconStyle = Icon ? getBareIconStyle(Icon) : undefined const navigable = editor.storage.mention?.navigable === true const workspaceId = typeof params.workspaceId === 'string' ? params.workspaceId : undefined const path = navigable && workspaceId ? simLinkPath(workspaceId, kind, id) : null @@ -55,7 +55,7 @@ export function MentionChipView({ node, editor }: ReactNodeViewProps) { onClick={path ? handleClick : undefined} title={label} > - + {Icon && } {label} ) diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/mention/mention-icon.test.ts b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/mention/mention-icon.test.ts index de6f15e613..1b8960913e 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/mention/mention-icon.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/mention/mention-icon.test.ts @@ -1,17 +1,29 @@ /** @vitest-environment node */ -import { Box, File } from 'lucide-react' +import { Workflow } from '@sim/emcn/icons' import { describe, expect, it } from 'vitest' +import { AgentSkillsIcon } from '@/components/icons' +import { getDocumentIcon } from '@/components/icons/document-icons' import { mentionIcon } from './mention-icon' import type { MentionKind } from './types' describe('mentionIcon', () => { - it('returns the category icon for a known kind', () => { - expect(mentionIcon('file', 'x')).toBe(File) + it('uses the product-wide glyph for a known kind', () => { + expect(mentionIcon('workflow', 'x')).toBe(Workflow) }) - it('falls back to a generic icon for an empty or unrecognized kind (never undefined)', () => { + it('uses the shared skills glyph, not a one-off icon', () => { + // The same glyph SkillTile and the chat context registry render. + expect(mentionIcon('skill', 'x')).toBe(AgentSkillsIcon) + }) + + it('derives the file icon from the filename extension', () => { + expect(mentionIcon('file', 'x', 'report.pdf')).toBe(getDocumentIcon('', 'report.pdf')) + expect(mentionIcon('file', 'x', 'data.csv')).toBe(getDocumentIcon('', 'data.csv')) + }) + + it('returns undefined for an unrecognized kind so callers render no icon', () => { // The schema default is '' and a sim: link could carry a future kind — neither may crash render. - expect(mentionIcon('' as unknown as MentionKind, 'x')).toBe(Box) - expect(mentionIcon('dataset' as unknown as MentionKind, 'x')).toBe(Box) + expect(mentionIcon('' as unknown as MentionKind, 'x')).toBeUndefined() + expect(mentionIcon('dataset' as unknown as MentionKind, 'x')).toBeUndefined() }) }) diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/mention/mention-icon.ts b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/mention/mention-icon.ts index c655ed0f74..828328efe6 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/mention/mention-icon.ts +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/mention/mention-icon.ts @@ -1,29 +1,43 @@ import type { ComponentType } from 'react' -import { Box, Database, File, Folder, Sparkles, Table, Workflow } from 'lucide-react' +import { Database, Folder, Table, Workflow } from '@sim/emcn/icons' +import { AgentSkillsIcon } from '@/components/icons' +import { getDocumentIcon } from '@/components/icons/document-icons' import { getBlock } from '@/blocks/registry' import type { MentionKind } from './types' -/** Icon component shape both the lucide kind-icons and the brand block icons satisfy. */ +/** Icon component shape both the kind icons and the brand block icons satisfy. */ export type MentionIcon = ComponentType<{ className?: string }> -const KIND_ICONS: Record, MentionIcon> = { - file: File, +/** + * The glyph each mention kind uses elsewhere in the product, so a mention reads + * as the resource it links to. Mirrors `CHAT_CONTEXT_KIND_REGISTRY`, the same + * mapping Chat's `@` menu renders. + */ +const KIND_ICONS: Record, MentionIcon> = { folder: Folder, table: Table, knowledge: Database, workflow: Workflow, - skill: Sparkles, + skill: AgentSkillsIcon, } /** - * Resolves the icon for a mention. Integrations use their brand icon from the block registry (keyed by - * blockType, which is the mention `id`), falling back to a generic icon if the block was since removed; - * every other kind uses a lucide category icon, falling back to the same generic icon for an empty or - * unrecognized kind (the schema default is `''`, and a `sim:` link could carry a kind a future version - * adds) — so the result is always a real component and the chip is never icon-less. Shared by the menu - * rows and the inserted chip so both render the same icon. + * Resolves the icon for a mention: + * + * - `integration` uses the block's brand icon from the registry, keyed by the + * mention `id` (the blockType). + * - `file` uses the extension-derived document icon, so a `.pdf` and a `.csv` + * look different — matching the file list and Chat's context chips. + * - every other kind uses its product-wide glyph. + * + * Returns `undefined` when nothing sensible applies — an unrecognized kind (the + * node schema defaults `kind` to `''`, and a hand-written `sim:` link can carry + * anything) or a block that has since been removed. Callers render no icon in + * that case rather than a meaningless placeholder, which is what the chat + * context registry does too. */ -export function mentionIcon(kind: MentionKind, id: string): MentionIcon { - if (kind === 'integration') return (getBlock(id)?.icon as MentionIcon | undefined) ?? Box - return KIND_ICONS[kind] ?? Box +export function mentionIcon(kind: MentionKind, id: string, label = ''): MentionIcon | undefined { + if (kind === 'integration') return getBlock(id)?.icon as MentionIcon | undefined + if (kind === 'file') return getDocumentIcon('', label) + return KIND_ICONS[kind as Exclude] } diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/mention/use-markdown-mentions.ts b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/mention/use-markdown-mentions.ts index 023faacc43..1451fffe7c 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/mention/use-markdown-mentions.ts +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/mention/use-markdown-mentions.ts @@ -55,7 +55,7 @@ export function useMarkdownMentions( id: file.id, label: file.name, group: 'Files', - icon: mentionIcon('file', file.id), + icon: mentionIcon('file', file.id, file.name), }) for (const folder of folders.data ?? []) items.push({ diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-editor.css b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-editor.css index 5c698d2583..cfb5e9a2ec 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-editor.css +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-editor.css @@ -438,12 +438,16 @@ } /* - * Field variant (modal embed): match the surrounding chip fields' typography exactly — + * Field variant (form embed): match the surrounding chip fields' typography exactly — * body at the chip `text-sm` (14px) scale and the placeholder at `--text-muted` (not the * lighter document `--text-subtle`), so the editor reads as one of the form's fields. + * The weight drops to a normal 400 as well: the document scale's 430 reads visibly + * thicker than a `ChipInput`/`ChipTextarea` beside it. Headings, `strong`, and `th` + * set their own 600, so only body text and the placeholder are affected. */ .rich-markdown-field-prose { font-size: 14px; + font-weight: 400; line-height: 22px; } diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-field.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-field.tsx index 8936c9a489..e0723b4d5c 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-field.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-field.tsx @@ -1,6 +1,6 @@ 'use client' -import { useEffect, useRef, useState } from 'react' +import { useEffect, useLayoutEffect, useRef, useState } from 'react' import { ChipTextarea, chipFieldSurfaceClass, cn } from '@sim/emcn' import type { JSONContent } from '@tiptap/core' import { EditorContent, useEditor } from '@tiptap/react' @@ -30,9 +30,14 @@ interface RichMarkdownFieldProps { /** True while `value` is being pushed in externally (AI generation) — the editor turns read-only and mirrors each update. */ isStreaming?: boolean autoFocus?: boolean - /** Min height of the scroll box in px. */ + /** Min height of the editor box in px. */ minHeight?: number - /** Max height of the scroll box in px before it scrolls. */ + /** + * Max height in px before the box scrolls internally. Set this inside a modal, + * where the surface itself cannot grow. Omit on a full-page surface so the + * editor grows with its content and the page owns the only scrollbar — a + * capped box stranded above empty page is worse than a long document. + */ maxHeight?: number /** Swaps the border to the error token (the message itself is rendered by the surrounding field). */ error?: boolean @@ -61,7 +66,7 @@ function LoadedRichMarkdownField({ isStreaming = false, autoFocus = false, minHeight = 140, - maxHeight = 360, + maxHeight, error = false, workspaceId, disableTagging, @@ -176,10 +181,17 @@ function LoadedRichMarkdownField({
@@ -206,12 +218,44 @@ function RawMarkdownField({ disabled = false, isStreaming = false, minHeight = 140, - maxHeight = 360, + maxHeight, error = false, onPasteText, }: RichMarkdownFieldProps) { + // Disabled-look without the `disabled` attribute — a disabled textarea is + // inert to wheel/scrollbar, but locked content must stay scrollable. + const lockedView = disabled && !isStreaming + + /** + * Uncapped, the textarea grows with its content so it matches the WYSIWYG + * path on a full-page surface — a textarea has no intrinsic auto-height, so + * the height is synced to `scrollHeight` on every value change. Capped (in a + * modal) it keeps its own scrollbar and this is skipped. + */ + const textareaRef = useRef(null) + const autoGrow = maxHeight === undefined + useLayoutEffect(() => { + if (!autoGrow) return + const el = textareaRef.current + if (!el) return + + const measure = () => { + el.style.height = 'auto' + el.style.height = `${Math.max(el.scrollHeight, minHeight)}px` + } + measure() + + // The box is `overflow-hidden` while uncapped, so a width change that + // re-wraps lines without touching `value` would clip the tail with no + // scrollbar to reach it — re-measure whenever the element resizes. + const observer = new ResizeObserver(measure) + observer.observe(el) + return () => observer.disconnect() + }, [autoGrow, value, minHeight]) + return ( onChange(event.target.value)} onPaste={(event) => { @@ -220,15 +264,20 @@ function RawMarkdownField({ }} placeholder={placeholder} error={error} - readOnly={disabled || isStreaming} + viewOnly={lockedView} + readOnly={isStreaming} + tabIndex={lockedView ? -1 : undefined} + className={cn(lockedView && 'select-none opacity-50', autoGrow && 'overflow-hidden')} style={{ minHeight, maxHeight }} /> ) } /** - * A controlled, string-valued markdown editor for modal fields. Drop it inside a `ChipModalField - * type='custom'`. Mirrors the file editor's safety gate (decided once from the initial value): + * A controlled, string-valued markdown editor. Inside a modal, drop it in a `ChipModalField + * type='custom'` and pass a `maxHeight` so it scrolls within the modal; on a full-page surface omit + * `maxHeight` so it grows with its content and the page owns the only scrollbar. + * Mirrors the file editor's safety gate (decided once from the initial value): * round-trip-safe content opens in the WYSIWYG editor, while lossy markdown (raw HTML, footnotes, * comments) falls back to raw-text editing so an edit can't silently drop those constructs. */ diff --git a/apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-skills-section.tsx b/apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-skills-section.tsx index 734d4a14c8..ea4fa847a2 100644 --- a/apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-skills-section.tsx +++ b/apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-skills-section.tsx @@ -6,6 +6,7 @@ import { Check, Plus } from 'lucide-react' import { usePostHog } from 'posthog-js/react' import { captureEvent } from '@/lib/posthog/client' import { SkillTile } from '@/app/workspace/[workspaceId]/components' +import { isSkillNameConflictError } from '@/app/workspace/[workspaceId]/skills/components/utils' import type { SuggestedSkill } from '@/blocks/types' import { useCreateSkill, useSkills } from '@/hooks/queries/skills' @@ -77,8 +78,15 @@ export function IntegrationSkillsSection({ position, skill_count: skills.length, }) - } catch { - toast.error(`Failed to add "${skill.name}" — please try again`) + } catch (error) { + // A name conflict just means the skill is already in this workspace — + // everyone with workspace access can already see and use it, so there is + // nothing to request and retrying can never succeed. + if (isSkillNameConflictError(error)) { + toast.error(`"${skill.name}" is already in this workspace`) + } else { + toast.error(`Failed to add "${skill.name}" — please try again`) + } } finally { inFlightRef.current.delete(skill.name) setPendingNames((prev) => { diff --git a/apps/sim/app/workspace/[workspaceId]/settings/[section]/search-params.ts b/apps/sim/app/workspace/[workspaceId]/settings/[section]/search-params.ts index 85b4a56b91..94aae59169 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/[section]/search-params.ts +++ b/apps/sim/app/workspace/[workspaceId]/settings/[section]/search-params.ts @@ -96,6 +96,22 @@ export const customBlockIdUrlKeys = { clearOnDefault: true, } as const +/** + * `custom-tool-id` deep-links the Custom Tools settings tab to a specific + * tool's detail sub-view. The "create new" flow stays in local state — only + * existing entities are deep-linkable. + */ +export const customToolIdParam = { + key: 'custom-tool-id', + parser: parseAsString, +} as const + +/** Opening a tool's detail is a destination → push to history; clear on close. */ +export const customToolIdUrlKeys = { + history: 'push', + clearOnDefault: true, +} as const + /** * `fork-direction` is the sync direction (push/pull) on the parent fork's detail * page — shareable view state, so a copied link opens the same side of the sync. diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/custom-tools/components/custom-tool-detail/custom-tool-detail.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/custom-tools/components/custom-tool-detail/custom-tool-detail.tsx new file mode 100644 index 0000000000..db3b2f47db --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/custom-tools/components/custom-tool-detail/custom-tool-detail.tsx @@ -0,0 +1,336 @@ +'use client' + +import { useMemo, useState } from 'react' +import { ChipConfirmModal, toast } from '@sim/emcn' +import { ArrowLeft, Wrench } from '@sim/emcn/icons' +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { ResourceTile } from '@/app/workspace/[workspaceId]/components' +import { + CredentialDetailHeading, + UnsavedChangesModal, +} from '@/app/workspace/[workspaceId]/components/credential-detail' +import { + CUSTOM_TOOL_DELETE_CONFIRM_TEXT, + CustomToolCodeField, + CustomToolSchemaField, + extractSchemaIdentity, + extractSchemaParameters, + FieldErrorText, + FUNCTION_NAME_LOCKED, + GeneratePromptControl, + useCodeGeneration, + useSchemaGeneration, + validateCustomToolSchema, +} from '@/app/workspace/[workspaceId]/components/custom-tool-editor' +import { saveDiscardActions } from '@/app/workspace/[workspaceId]/settings/components/save-discard-actions/save-discard-actions' +import type { SettingsAction } from '@/app/workspace/[workspaceId]/settings/components/settings-header/settings-header' +import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel' +import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section' +import { useSettingsUnsavedGuard } from '@/app/workspace/[workspaceId]/settings/hooks/use-settings-unsaved-guard' +import type { CustomToolDefinition } from '@/hooks/queries/custom-tools' +import { + useCreateCustomTool, + useDeleteCustomTool, + useUpdateCustomTool, +} from '@/hooks/queries/custom-tools' + +const logger = createLogger('CustomToolDetail') + +interface CustomToolDetailProps { + workspaceId: string + /** `null` on the create flow, which starts from an empty draft. */ + tool: CustomToolDefinition | null + /** Viewers without edit rights get the same page with every control inert. */ + readOnly?: boolean + onBack: () => void + /** Lands the caller on the tool it just created, matching the skill create flow. */ + onCreated?: (toolId: string) => void +} + +/** + * Full-page custom tool editor rendered as a settings detail sub-view: a back + * chip, dirty-gated Discard/Save, Delete, and the Schema and Code editors + * stacked (no tabs — the page has room for both). Uses the same fields as the + * canvas modal so the two surfaces never drift. + */ +export function CustomToolDetail({ + workspaceId, + tool, + readOnly = false, + onBack, + onCreated, +}: CustomToolDetailProps) { + const isEditing = !!tool + + const createTool = useCreateCustomTool() + const updateTool = useUpdateCustomTool() + const deleteTool = useDeleteCustomTool() + + /** + * The dirty baseline. Seeded once at mount — the list keys this view by tool + * id, so picking a different tool remounts it — and moved only by an explicit + * save. A background list refetch must never shift it out from under + * in-progress edits. + */ + const [seededSchema, setSeededSchema] = useState(() => + tool ? JSON.stringify(tool.schema, null, 2) : '' + ) + const [seededCode, setSeededCode] = useState(() => tool?.code ?? '') + + const [jsonSchema, setJsonSchema] = useState(seededSchema) + const [functionCode, setFunctionCode] = useState(seededCode) + const [schemaError, setSchemaError] = useState(null) + const [codeError, setCodeError] = useState(null) + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false) + + const schemaParameters = useMemo(() => extractSchemaParameters(jsonSchema), [jsonSchema]) + + /** + * Heading reflects the draft schema, so the tool names itself as you type + * rather than hiding its identity inside the JSON. Falls back to the saved + * tool while the draft is mid-edit and unparseable. + */ + const identity = useMemo(() => extractSchemaIdentity(jsonSchema), [jsonSchema]) + + const schemaGeneration = useSchemaGeneration({ + jsonSchema, + setJsonSchema: (updater) => { + setJsonSchema(updater) + setSchemaError(null) + }, + replaceJsonSchema: (value) => { + setJsonSchema(value) + setSchemaError(null) + }, + }) + + const codeGeneration = useCodeGeneration({ + functionCode, + schemaParameters, + setFunctionCode: (updater) => { + setFunctionCode(updater) + setCodeError(null) + }, + replaceFunctionCode: (value) => { + setFunctionCode(value) + setCodeError(null) + }, + }) + + const dirty = isEditing + ? jsonSchema !== seededSchema || functionCode !== seededCode + : jsonSchema.trim().length > 0 || functionCode.trim().length > 0 + + const guard = useSettingsUnsavedGuard({ isDirty: dirty }) + + const saving = createTool.isPending || updateTool.isPending + const isSchemaValid = useMemo(() => validateCustomToolSchema(jsonSchema).isValid, [jsonSchema]) + const streaming = schemaGeneration.isStreaming || codeGeneration.isStreaming + + const handleDiscard = () => { + setJsonSchema(seededSchema) + setFunctionCode(seededCode) + setSchemaError(null) + setCodeError(null) + } + + const handleSave = async () => { + if (saving) return + + if (!jsonSchema.trim()) { + setSchemaError('Schema cannot be empty') + return + } + + const { isValid, error } = validateCustomToolSchema(jsonSchema) + if (!isValid) { + setSchemaError(error) + return + } + + setSchemaError(null) + setCodeError(null) + + const schema = JSON.parse(jsonSchema) + const title = schema.function.name + + try { + if (tool) { + await updateTool.mutateAsync({ + workspaceId, + toolId: tool.id, + updates: { title, schema, code: functionCode }, + }) + // Saving an edit keeps you on the tool (matching the other settings + // detail views); re-baseline so Discard/Save drop back out of the header. + setSeededSchema(jsonSchema) + setSeededCode(functionCode) + } else { + const created = await createTool.mutateAsync({ + workspaceId, + tool: { title, schema, code: functionCode }, + }) + // The upsert responds with the workspace's whole tool list (newest + // first), not just the new row — match by title rather than index. + const createdId = created.find((t) => t.title === title)?.id + if (createdId) onCreated?.(createdId) + else onBack() + } + } catch (error) { + logger.error('Failed to save custom tool', error) + const message = getErrorMessage(error, 'Failed to save custom tool') + setSchemaError( + message.includes('Cannot change function name') ? FUNCTION_NAME_LOCKED : message + ) + } + } + + const handleConfirmDelete = async () => { + if (!tool) return + setShowDeleteConfirm(false) + try { + await deleteTool.mutateAsync({ workspaceId, toolId: tool.id }) + onBack() + } catch (error) { + logger.error('Failed to delete custom tool', error) + toast.error("Couldn't delete tool", { + description: getErrorMessage(error, 'Please try again in a moment.'), + }) + } + } + + /** + * On create, the primary action is always visible so the page announces what + * it is for — disabled until the schema is a valid function definition. + * (`saveDiscardActions` is dirty-gated and would render nothing on an empty + * draft.) Discard still only appears once there is something to discard. + */ + const createToolActions: SettingsAction[] = [ + ...(dirty ? [{ text: 'Discard', onSelect: handleDiscard, disabled: saving }] : []), + { + text: saving ? 'Creating...' : 'Create', + variant: 'primary' as const, + onSelect: handleSave, + disabled: saving || streaming || !isSchemaValid, + }, + ] + + return ( + <> + guard.guardBack(onBack) }} + title={identity.name || tool?.title || 'New tool'} + actions={[ + ...(readOnly + ? [] + : isEditing + ? saveDiscardActions({ + dirty, + saving, + onSave: handleSave, + onDiscard: handleDiscard, + saveDisabled: !isSchemaValid || streaming, + }) + : createToolActions), + ...(tool && !readOnly + ? [ + { + text: deleteTool.isPending ? 'Deleting...' : 'Delete', + variant: 'destructive' as const, + onSelect: () => setShowDeleteConfirm(true), + disabled: deleteTool.isPending, + }, + ] + : []), + ]} + > +
+ } + title={identity.name || tool?.title || 'New tool'} + subtitle={ + identity.description || + tool?.schema.function.description || + 'Define the JSON schema your agents call, and the code that runs.' + } + /> + + {schemaError} : undefined + } + action={ + readOnly ? undefined : ( + schemaGeneration.generateStream({ prompt })} + /> + ) + } + > + { + setJsonSchema(value) + setSchemaError(value.trim() ? validateCustomToolSchema(value).error : null) + }} + error={!!schemaError} + generation={schemaGeneration} + disabled={readOnly} + /> + + + {codeError} : undefined} + action={ + readOnly ? undefined : ( + codeGeneration.generateStream({ prompt })} + /> + ) + } + > + { + setFunctionCode(value) + if (codeError) setCodeError(null) + }} + error={!!codeError} + generation={codeGeneration} + schemaParameters={schemaParameters} + workspaceId={workspaceId} + disabled={readOnly} + /> + +
+
+ + + + + + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/custom-tools/components/custom-tool-detail/index.ts b/apps/sim/app/workspace/[workspaceId]/settings/components/custom-tools/components/custom-tool-detail/index.ts new file mode 100644 index 0000000000..ffc8ae4ff1 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/custom-tools/components/custom-tool-detail/index.ts @@ -0,0 +1 @@ +export { CustomToolDetail } from '@/app/workspace/[workspaceId]/settings/components/custom-tools/components/custom-tool-detail/custom-tool-detail' diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/custom-tools/custom-tools.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/custom-tools/custom-tools.tsx index 2a5e789da0..95ce91a808 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/custom-tools/custom-tools.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/custom-tools/custom-tools.tsx @@ -1,22 +1,24 @@ 'use client' import { useState } from 'react' -import { ChipConfirmModal } from '@sim/emcn' -import { createLogger } from '@sim/logger' +import { Wrench } from '@sim/emcn/icons' import { getErrorMessage } from '@sim/utils/errors' -import { Plus } from 'lucide-react' +import { ArrowRight, Plus } from 'lucide-react' import { useParams } from 'next/navigation' +import { useQueryState } from 'nuqs' import { canMutateWorkspaceSettingsSection } from '@/components/settings/navigation' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' -import { RowActionsMenu } from '@/app/workspace/[workspaceId]/settings/components/row-actions-menu' +import { + customToolIdParam, + customToolIdUrlKeys, +} from '@/app/workspace/[workspaceId]/settings/[section]/search-params' +import { CustomToolDetail } from '@/app/workspace/[workspaceId]/settings/components/custom-tools/components/custom-tool-detail' import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state' import type { SettingsAction } from '@/app/workspace/[workspaceId]/settings/components/settings-header/settings-header' import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel' +import { SettingsResourceRow } from '@/app/workspace/[workspaceId]/settings/components/settings-resource-row' import { useSettingsSearch } from '@/app/workspace/[workspaceId]/settings/components/use-settings-search' -import { CustomToolModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/custom-tool-modal/custom-tool-modal' -import { useCustomTools, useDeleteCustomTool } from '@/hooks/queries/custom-tools' - -const logger = createLogger('CustomToolsSettings') +import { useCustomTools } from '@/hooks/queries/custom-tools' export function CustomTools() { const params = useParams() @@ -24,15 +26,22 @@ export function CustomTools() { const workspacePermissions = useUserPermissionsContext() const canEdit = canMutateWorkspaceSettingsSection('custom-tools', workspacePermissions) - const { data: tools = [], isLoading, error, refetch: refetchTools } = useCustomTools(workspaceId) - const deleteToolMutation = useDeleteCustomTool() + const { data: tools = [], isLoading, error } = useCustomTools(workspaceId) const [searchTerm, setSearchTerm] = useSettingsSearch() - const [deletingTools, setDeletingTools] = useState>(() => new Set()) - const [editingTool, setEditingTool] = useState(null) - const [showAddForm, setShowAddForm] = useState(false) - const [toolToDelete, setToolToDelete] = useState<{ id: string; name: string } | null>(null) - const [showDeleteDialog, setShowDeleteDialog] = useState(false) + const [selectedToolId, setSelectedToolId] = useQueryState(customToolIdParam.key, { + ...customToolIdParam.parser, + ...customToolIdUrlKeys, + }) + /** The create flow has no entity id and is not deep-linkable — stays local. */ + const [isCreating, setIsCreating] = useState(false) + + const selectedTool = selectedToolId ? tools.find((t) => t.id === selectedToolId) : undefined + + const closeDetail = () => { + setIsCreating(false) + void setSelectedToolId(null, { history: 'replace' }) + } const filteredTools = tools.filter((tool) => { if (!searchTerm.trim()) return true @@ -44,52 +53,7 @@ export function CustomTools() { ) }) - const handleDeleteClick = (toolId: string) => { - const tool = tools.find((t) => t.id === toolId) - if (!tool) return - - setToolToDelete({ - id: toolId, - name: tool.title || tool.schema?.function?.name || 'this custom tool', - }) - setShowDeleteDialog(true) - } - - const handleDeleteTool = async () => { - if (!toolToDelete) return - - const tool = tools.find((t) => t.id === toolToDelete.id) - if (!tool) return - - setDeletingTools((prev) => new Set(prev).add(toolToDelete.id)) - setShowDeleteDialog(false) - - try { - await deleteToolMutation.mutateAsync({ - workspaceId: tool.workspaceId ?? null, - toolId: toolToDelete.id, - }) - logger.info(`Deleted custom tool: ${toolToDelete.id}`) - } catch (error) { - logger.error('Error deleting custom tool:', error) - } finally { - setDeletingTools((prev) => { - const next = new Set(prev) - next.delete(toolToDelete.id) - return next - }) - setToolToDelete(null) - } - } - - const handleToolSaved = () => { - setShowAddForm(false) - setEditingTool(null) - refetchTools() - } - - const hasTools = tools && tools.length > 0 - const showEmptyState = !hasTools && !showAddForm && !editingTool + const showEmptyState = tools.length === 0 const showNoResults = searchTerm.trim() && filteredTools.length === 0 && tools.length > 0 const actions: SettingsAction[] = canEdit @@ -98,115 +62,81 @@ export function CustomTools() { text: 'Add tool', icon: Plus, variant: 'primary', - onSelect: () => setShowAddForm(true), + onSelect: () => setIsCreating(true), disabled: isLoading, }, ] : [] - return ( - <> - { + setIsCreating(false) + void setSelectedToolId(toolId) }} - actions={actions} - > - {error ? ( -
-

- {getErrorMessage(error, 'Failed to load tools')} -

-
- ) : isLoading ? null : showEmptyState ? ( - - {canEdit ? 'Click "Add tool" above to get started' : 'No custom tools configured'} - - ) : ( -
- {filteredTools.map((tool) => ( -
-
- - {tool.title || 'Unnamed Tool'} - - {tool.schema?.function?.description && ( -

- {tool.schema.function.description} -

- )} -
- {canEdit && ( -
- setEditingTool(tool.id) }, - { - label: 'Delete', - destructive: true, - disabled: deletingTools.has(tool.id), - onSelect: () => handleDeleteClick(tool.id), - }, - ]} - /> -
- )} -
- ))} - {showNoResults && ( - - No tools found matching "{searchTerm}" - - )} -
- )} -
+ /> + ) + } - {canEdit && ( - { - if (!open) { - setShowAddForm(false) - setEditingTool(null) - } - }} - onSave={handleToolSaved} - onDelete={() => {}} - blockId='' - initialValues={ - editingTool - ? (() => { - const tool = tools.find((t) => t.id === editingTool) - return tool?.schema - ? { id: tool.id, schema: tool.schema, code: tool.code } - : undefined - })() - : undefined - } - /> + return ( + + {error ? ( +
+

+ {getErrorMessage(error, 'Failed to load tools')} +

+
+ ) : isLoading ? null : showEmptyState ? ( + + {canEdit ? 'Click "Add tool" above to get started' : 'No custom tools configured'} + + ) : ( +
+ {filteredTools.map((tool) => ( + + ))} + {showNoResults && ( + + No tools found matching "{searchTerm}" + + )} +
)} - - {canEdit && ( - { - if (!open) setShowDeleteDialog(false) - }} - srTitle='Delete Custom Tool' - title='Delete Custom Tool' - text={[ - 'Are you sure you want to delete ', - { text: toolToDelete?.name ?? 'this tool', bold: true }, - '? This action cannot be undone.', - ]} - confirm={{ label: 'Delete', onClick: handleDeleteTool }} - /> - )} - +
) } diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/settings-resource-row/settings-resource-row.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/settings-resource-row/settings-resource-row.tsx index 88d9365e65..7e31036010 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/settings-resource-row/settings-resource-row.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/settings-resource-row/settings-resource-row.tsx @@ -20,6 +20,11 @@ interface SettingsResourceRowProps { * normalize to 20px so a fallback icon doesn't balloon. */ iconFill?: boolean + /** + * Fills the tile like the skills/tools resource tiles instead of the default + * page-background tile, so a settings list can match its gallery counterpart. + */ + iconFilled?: boolean /** Primary line — truncates. */ title: ReactNode /** Secondary muted line — truncates. */ @@ -29,11 +34,12 @@ interface SettingsResourceRowProps { } const TILE_BASE = - 'flex size-9 flex-shrink-0 items-center justify-center overflow-hidden rounded-xl border border-[var(--border-1)] bg-[var(--bg)] [&_svg]:size-5' + 'flex size-9 flex-shrink-0 items-center justify-center overflow-hidden rounded-xl border border-[var(--border-1)] [&_svg]:size-5' export function SettingsResourceRow({ icon, iconFill = false, + iconFilled = false, title, description, trailing, @@ -41,7 +47,13 @@ export function SettingsResourceRow({ return (
-
+
{icon}
diff --git a/apps/sim/app/workspace/[workspaceId]/skills/[skillId]/components/skill-editors-card.tsx b/apps/sim/app/workspace/[workspaceId]/skills/[skillId]/components/skill-editors-card.tsx new file mode 100644 index 0000000000..4cdb2d4a9e --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/skills/[skillId]/components/skill-editors-card.tsx @@ -0,0 +1,59 @@ +'use client' + +import { + MemberRow, + SKILL_EDITOR_ROLE_OPTIONS, + type SkillEditorRole, + skillEditorLockReason, +} from '@/components/permissions' +import { DetailSection } from '@/app/workspace/[workspaceId]/components/credential-detail' +import type { SkillEditorsController } from '@/app/workspace/[workspaceId]/skills/components/skill-members' + +interface SkillEditorsCardProps { + editors: SkillEditorsController + /** Whether the viewer can edit the skill (and therefore manage its editors). */ + canEdit: boolean +} + +/** + * Page-styled editor roster for the skill detail page: workspace admins + * (derived, always editors) and explicitly added editors. Everyone in the + * workspace can already see and use the skill — this list gates editing only. + * Adding people happens through the header Share action. + * + * Rows are the shared {@link MemberRow} so the roster is chrome-identical to the + * credential detail surface. Skill membership is binary, so the role control + * carries a single `Editor` option and is disabled on every row; a derived + * (workspace-admin) row also locks Remove and explains the inheritance on hover. + */ +export function SkillEditorsCard({ editors, canEdit }: SkillEditorsCardProps) { + return ( + + {editors.editorsError ? ( + + Couldn't load editors. You may no longer have access to this skill. + + ) : editors.editorsLoading ? null : ( +
+ {editors.editors.map((editor) => ( + + key={editor.id} + member={{ + userId: editor.userId, + userName: editor.userName, + userEmail: editor.userEmail, + role: 'editor', + }} + roleOptions={SKILL_EDITOR_ROLE_OPTIONS} + lockReason={skillEditorLockReason(editor.isWorkspaceAdmin)} + canManage={canEdit} + roleDisabled + removeDisabled={editor.isWorkspaceAdmin} + onRemove={() => editors.removeEditor(editor.userId)} + /> + ))} +
+ )} +
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/skills/[skillId]/page.tsx b/apps/sim/app/workspace/[workspaceId]/skills/[skillId]/page.tsx new file mode 100644 index 0000000000..5f22dccfcf --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/skills/[skillId]/page.tsx @@ -0,0 +1,15 @@ +import type { Metadata } from 'next' +import { SkillDetail } from '@/app/workspace/[workspaceId]/skills/[skillId]/skill-detail' + +export const metadata: Metadata = { + title: 'Skill', +} + +export default async function SkillDetailPage({ + params, +}: { + params: Promise<{ workspaceId: string; skillId: string }> +}) { + const { workspaceId, skillId } = await params + return +} diff --git a/apps/sim/app/workspace/[workspaceId]/skills/[skillId]/skill-detail.tsx b/apps/sim/app/workspace/[workspaceId]/skills/[skillId]/skill-detail.tsx new file mode 100644 index 0000000000..9029bbda79 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/skills/[skillId]/skill-detail.tsx @@ -0,0 +1,356 @@ +'use client' + +import { type ReactNode, useState } from 'react' +import { + Chip, + ChipConfirmModal, + ChipInput, + ChipLink, + ChipTextarea, + chipFieldSurfaceClass, + cn, + Send, + Tooltip, + toast, +} from '@sim/emcn' +import { ArrowLeft } from '@sim/emcn/icons' +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import dynamic from 'next/dynamic' +import { useRouter } from 'next/navigation' +import { AddPeopleModal } from '@/components/permissions' +import { SkillTile } from '@/app/workspace/[workspaceId]/components' +import { + CredentialDetailHeading, + CredentialDetailLayout, + DetailSection, + UnsavedChangesModal, + useUnsavedChangesGuard, +} from '@/app/workspace/[workspaceId]/components/credential-detail' +import { SkillEditorsCard } from '@/app/workspace/[workspaceId]/skills/[skillId]/components/skill-editors-card' +import { useSkillEditorsController } from '@/app/workspace/[workspaceId]/skills/components/skill-members' +import { + isSkillNameConflictError, + parseSkillMarkdown, + validateSkillName, +} from '@/app/workspace/[workspaceId]/skills/components/utils' +import { useDeleteSkill, useSkills, useUpdateSkill } from '@/hooks/queries/skills' + +const RichMarkdownField = dynamic( + () => + import( + '@/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-field' + ).then((m) => m.RichMarkdownField), + { + ssr: false, + loading: () =>
, + } +) + +const logger = createLogger('SkillDetail') + +interface FieldErrors { + name?: string + description?: string + content?: string +} + +interface FieldLockTooltipProps { + reason: string | null + children: ReactNode +} + +/** + * Wraps a read-only field so hovering it explains why editing is locked. + * Renders children unchanged when the field is editable. The wrapper div + * receives the hover events a disabled control swallows. + */ +function FieldLockTooltip({ reason, children }: FieldLockTooltipProps) { + if (!reason) return <>{children} + return ( + + +
{children}
+
+ {reason} +
+ ) +} + +interface SkillDetailProps { + workspaceId: string + skillId: string +} + +/** + * Full-page skill detail, mirroring the integration credential detail surface: + * a fixed action bar (Share / Delete / Save), a heading, editable Name / + * Description / Content sections, and the Skill Editors roster. Non-editors + * and built-in template skills render read-only. + */ +export function SkillDetail({ workspaceId, skillId }: SkillDetailProps) { + const router = useRouter() + const skillsHref = `/workspace/${workspaceId}/skills` + + const { data: skills = [], isPending: skillsLoading } = useSkills(workspaceId) + const updateSkill = useUpdateSkill() + const deleteSkill = useDeleteSkill() + const skill = skills.find((s) => s.id === skillId) ?? null + const isBuiltin = !!skill?.readOnly + const editors = useSkillEditorsController({ + skillId, + workspaceId, + // Built-ins have no editors; skip the roster fetch (it would 404). + enabled: !!skill && !isBuiltin, + }) + const canEdit = !isBuiltin && !!skill?.canEdit + + const [nameDraft, setNameDraft] = useState('') + const [descriptionDraft, setDescriptionDraft] = useState('') + const [contentDraft, setContentDraft] = useState('') + /** Bumped to remount the seed-once rich Content editor on programmatic sets. */ + const [contentSeed, setContentSeed] = useState(0) + const [errors, setErrors] = useState({}) + const [shareOpen, setShareOpen] = useState(false) + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false) + const [prevSkillId, setPrevSkillId] = useState(null) + + // Seed drafts when the skill first resolves (or the route id changes); a + // background refetch of the same skill must not clobber an in-progress edit. + if (skill && skill.id !== prevSkillId) { + setPrevSkillId(skill.id) + setNameDraft(skill.name) + setDescriptionDraft(skill.description) + setContentDraft(skill.content) + setErrors({}) + setContentSeed((seed) => seed + 1) + } + + const isDirty = + !!skill && + !isBuiltin && + (nameDraft !== skill.name || + descriptionDraft !== skill.description || + contentDraft !== skill.content) + + const guard = useUnsavedChangesGuard({ isDirty, backHref: skillsHref }) + + const handleSave = async () => { + if (!skill || !canEdit || !isDirty || updateSkill.isPending) return + + const newErrors: FieldErrors = {} + const nameError = validateSkillName(nameDraft) + if (nameError) newErrors.name = nameError + if (!descriptionDraft.trim()) newErrors.description = 'Description is required' + if (!contentDraft.trim()) newErrors.content = 'Content is required' + if (Object.keys(newErrors).length > 0) { + setErrors(newErrors) + return + } + + try { + // Partial update: only the fields that changed go over the wire. + await updateSkill.mutateAsync({ + workspaceId, + skillId: skill.id, + updates: { + ...(nameDraft !== skill.name ? { name: nameDraft } : {}), + ...(descriptionDraft !== skill.description ? { description: descriptionDraft } : {}), + ...(contentDraft !== skill.content ? { content: contentDraft } : {}), + }, + }) + setErrors({}) + } catch (error) { + if (isSkillNameConflictError(error)) { + setErrors({ name: getErrorMessage(error, 'This skill name is already taken.') }) + } else { + toast.error("Couldn't save skill", { + description: getErrorMessage(error, 'Please try again in a moment.'), + }) + } + logger.error('Failed to save skill', error) + } + } + + const handleConfirmDelete = async () => { + if (!skill) return + setShowDeleteConfirm(false) + try { + await deleteSkill.mutateAsync({ workspaceId, skillId: skill.id }) + router.push(skillsHref) + } catch (error) { + logger.error('Failed to delete skill', error) + } + } + + /** + * Pasting a full SKILL.md destructures it into the fields. Gated on a real + * YAML `name:` key so a stray `---` or heading-only snippet pastes as + * ordinary content instead of silently overwriting all three fields. + */ + const handleContentPaste = (text: string): boolean => { + const parsed = parseSkillMarkdown(text) + if (!parsed.nameFromFrontmatter) return false + setNameDraft(parsed.name) + setDescriptionDraft(parsed.description) + setContentDraft(parsed.content) + setErrors({}) + setContentSeed((seed) => seed + 1) + return true + } + + const back = ( + + Skills + + ) + + const actions = + skill && canEdit ? ( + <> + setShareOpen(true)}> + Share + + setShowDeleteConfirm(true)} disabled={deleteSkill.isPending}> + Delete + + + {updateSkill.isPending ? 'Saving...' : 'Save'} + + + ) : null + + if (skillsLoading && !skill) { + return ( + +

Loading…

+
+ ) + } + + if (!skill) { + return ( + +

Skill not found.

+
+ ) + } + + const readOnly = isBuiltin || !canEdit + const lockReason = !readOnly + ? null + : isBuiltin + ? 'Built-in skills are read-only' + : 'You need to be a skill editor to edit this skill' + + return ( + <> + + } + title={skill.name} + subtitle={isBuiltin ? 'Built-in skill' : skill.description} + /> + + + + { + setNameDraft(event.target.value) + if (errors.name) setErrors((prev) => ({ ...prev, name: undefined })) + }} + placeholder='my-skill-name' + autoComplete='off' + data-lpignore='true' + disabled={readOnly} + error={!!errors.name} + /> + + {errors.name && ( +

{errors.name}

+ )} +
+ + + + { + setDescriptionDraft(event.target.value) + if (errors.description) setErrors((prev) => ({ ...prev, description: undefined })) + }} + placeholder='What this skill does and when to use it...' + maxLength={1024} + autoComplete='off' + data-lpignore='true' + disabled={readOnly} + /> + + {errors.description && ( +

{errors.description}

+ )} +
+ + + + { + setContentDraft(value) + if (errors.content) setErrors((prev) => ({ ...prev, content: undefined })) + }} + placeholder='Skill instructions in markdown...' + minHeight={260} + disabled={readOnly} + error={!!errors.content} + workspaceId={workspaceId} + onPasteText={handleContentPaste} + /> + + {errors.content && ( +

{errors.content}

+ )} +
+ + {!isBuiltin && } +
+ + + + + + + + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/skills/components/skill-import/index.ts b/apps/sim/app/workspace/[workspaceId]/skills/components/skill-import/index.ts index a3408dbd9e..425a026a85 100644 --- a/apps/sim/app/workspace/[workspaceId]/skills/components/skill-import/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/skills/components/skill-import/index.ts @@ -1 +1,2 @@ export { SkillImport } from '@/app/workspace/[workspaceId]/skills/components/skill-import/skill-import' +export { SkillImportButton } from '@/app/workspace/[workspaceId]/skills/components/skill-import/skill-import-button' diff --git a/apps/sim/app/workspace/[workspaceId]/skills/components/skill-import/skill-import-button.tsx b/apps/sim/app/workspace/[workspaceId]/skills/components/skill-import/skill-import-button.tsx new file mode 100644 index 0000000000..02d7e61626 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/skills/components/skill-import/skill-import-button.tsx @@ -0,0 +1,58 @@ +'use client' + +import { useRef, useState } from 'react' +import { Chip, Loader, toast } from '@sim/emcn' +import { getErrorMessage } from '@sim/utils/errors' +import { + type ParsedSkill, + readSkillFile, + SKILL_IMPORT_ACCEPT, +} from '@/app/workspace/[workspaceId]/skills/components/utils' + +interface SkillImportButtonProps { + onImport: (data: ParsedSkill) => void + disabled?: boolean +} + +/** + * Header action that imports an existing SKILL.md into the create form. Opens + * the OS file picker directly — no field on the page — and reports failures as + * a toast, since the action bar has no room for an inline error. + */ +export function SkillImportButton({ onImport, disabled = false }: SkillImportButtonProps) { + const inputRef = useRef(null) + const [importing, setImporting] = useState(false) + + const handleFile = async (file: File) => { + setImporting(true) + try { + onImport(await readSkillFile(file)) + } catch (error) { + toast.error("Couldn't import skill", { + description: getErrorMessage(error, 'Please try a .md or .zip file.'), + }) + } finally { + setImporting(false) + } + } + + return ( + <> + inputRef.current?.click()} disabled={disabled || importing}> + {importing ? : 'Import'} + + { + const file = event.target.files?.[0] + event.target.value = '' + if (file) void handleFile(file) + }} + /> + + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/skills/components/skill-import/skill-import.tsx b/apps/sim/app/workspace/[workspaceId]/skills/components/skill-import/skill-import.tsx index a49e5c2280..4a06071c85 100644 --- a/apps/sim/app/workspace/[workspaceId]/skills/components/skill-import/skill-import.tsx +++ b/apps/sim/app/workspace/[workspaceId]/skills/components/skill-import/skill-import.tsx @@ -1,156 +1,52 @@ 'use client' -import { useCallback, useState } from 'react' -import { Chip, ChipInput, ChipModalField, Loader } from '@sim/emcn' +import { useState } from 'react' +import { ChipModalField } from '@sim/emcn' import { getErrorMessage } from '@sim/utils/errors' -import { requestJson } from '@/lib/api/client/request' -import { importSkillContract } from '@/lib/api/contracts' import { - extractSkillFromZip, - parseSkillMarkdown, + type ParsedSkill, + readSkillFile, + SKILL_IMPORT_ACCEPT, } from '@/app/workspace/[workspaceId]/skills/components/utils' -interface ImportedSkill { - name: string - description: string - content: string -} - interface SkillImportProps { - onImport: (data: ImportedSkill) => void -} - -type ImportState = 'idle' | 'loading' | 'error' - -const ACCEPTED_EXTENSIONS = ['.md', '.zip'] - -function isAcceptedFile(file: File): boolean { - const name = file.name.toLowerCase() - return ACCEPTED_EXTENSIONS.some((ext) => name.endsWith(ext)) + onImport: (data: ParsedSkill) => void } +/** + * The canvas modal's Import tab: a single file field that reads a SKILL.md (or + * a ZIP containing one) into the create form. The full-page create surface uses + * {@link SkillImportButton} in its action bar instead of a field. + */ export function SkillImport({ onImport }: SkillImportProps) { - const [githubUrl, setGithubUrl] = useState('') - const [githubState, setGithubState] = useState('idle') - const [githubError, setGithubError] = useState('') + const [importing, setImporting] = useState(false) + const [error, setError] = useState('') - const [fileState, setFileState] = useState('idle') - const [fileError, setFileError] = useState('') - - const handleGithubImport = useCallback(async () => { - const trimmed = githubUrl.trim() - if (!trimmed) { - setGithubError('Please enter a GitHub URL') - setGithubState('error') - return - } - - setGithubState('loading') - setGithubError('') + const handleFiles = async (files: File[]) => { + const file = files[0] + if (!file) return + setImporting(true) + setError('') try { - const data = await requestJson(importSkillContract, { body: { url: trimmed } }) - const parsed = parseSkillMarkdown(data.content) - setGithubState('idle') - onImport(parsed) + onImport(await readSkillFile(file)) } catch (err) { - const message = getErrorMessage(err, 'Failed to import from GitHub') - setGithubError(message) - setGithubState('error') + setError(getErrorMessage(err, 'Failed to process file')) + } finally { + setImporting(false) } - }, [githubUrl, onImport]) - - const processFile = useCallback( - async (file: File) => { - if (!isAcceptedFile(file)) { - setFileError('Unsupported file type. Use .md or .zip files.') - setFileState('error') - return - } - - setFileState('loading') - setFileError('') - - try { - let rawContent: string - - if (file.name.toLowerCase().endsWith('.zip')) { - if (file.size > 5 * 1024 * 1024) { - setFileError('ZIP file is too large (max 5 MB)') - setFileState('error') - return - } - rawContent = await extractSkillFromZip(file) - } else { - rawContent = await file.text() - } - - const parsed = parseSkillMarkdown(rawContent) - setFileState('idle') - onImport(parsed) - } catch (err) { - const message = getErrorMessage(err, 'Failed to process file') - setFileError(message) - setFileState('error') - } - }, - [onImport] - ) - - const handleFiles = useCallback( - (files: File[]) => { - const file = files[0] - if (file) processFile(file) - }, - [processFile] - ) + } return ( -
- -
- { - setGithubUrl(e.target.value) - if (githubError) setGithubError('') - }} - disabled={githubState === 'loading'} - className='min-w-0 flex-1' - /> - - {githubState === 'loading' ? : 'Fetch'} - -
-
- - - - -
- ) -} - -function ImportDivider() { - return ( -
-
- or -
-
+ void handleFiles(files)} + loading={importing} + label={importing ? 'Importing…' : undefined} + description='.md file with YAML frontmatter, or .zip containing a SKILL.md' + error={error || undefined} + /> ) } diff --git a/apps/sim/app/workspace/[workspaceId]/skills/components/skill-members/index.ts b/apps/sim/app/workspace/[workspaceId]/skills/components/skill-members/index.ts new file mode 100644 index 0000000000..18d6f7716c --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/skills/components/skill-members/index.ts @@ -0,0 +1,4 @@ +export { + type SkillEditorsController, + useSkillEditorsController, +} from './use-skill-editors-controller' diff --git a/apps/sim/app/workspace/[workspaceId]/skills/components/skill-members/use-skill-editors-controller.ts b/apps/sim/app/workspace/[workspaceId]/skills/components/skill-members/use-skill-editors-controller.ts new file mode 100644 index 0000000000..bcc2e6483f --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/skills/components/skill-members/use-skill-editors-controller.ts @@ -0,0 +1,76 @@ +'use client' + +import { useCallback, useMemo } from 'react' +import { createLogger } from '@sim/logger' +import type { AddPeopleTarget } from '@/components/permissions' +import type { SkillEditor } from '@/lib/api/contracts' +import { useRemoveSkillMember, useSkillMembers, useUpsertSkillMember } from '@/hooks/queries/skills' + +const logger = createLogger('SkillEditorsController') + +export interface SkillEditorsController { + editors: SkillEditor[] + editorsLoading: boolean + editorsError: boolean + /** Lowercased emails already on the roster (incl. workspace admins) — feeds the Add People modal. */ + existingEditorEmails: Set + addEditor: (target: AddPeopleTarget) => Promise + removeEditor: (userId: string) => Promise +} + +interface UseSkillEditorsControllerParams { + skillId: string + workspaceId: string + /** Gate the roster fetch off (e.g. built-in template skills have no editors). */ + enabled?: boolean +} + +/** + * Data + mutation controller behind the skill editor surfaces (the detail + * page's Skill Editors section and the Share modal): exposes the roster — + * explicit editors plus derived workspace admins — and the add/remove actions. + * Renderers own only chrome. + */ +export function useSkillEditorsController({ + skillId, + workspaceId, + enabled = true, +}: UseSkillEditorsControllerParams): SkillEditorsController { + const { + data: editors = [], + isPending: editorsLoading, + isError: editorsError, + } = useSkillMembers(skillId, { enabled }) + const { mutateAsync: upsertEditorAsync } = useUpsertSkillMember() + const { mutateAsync: removeEditorAsync } = useRemoveSkillMember() + + const existingEditorEmails = useMemo( + () => new Set(editors.map((editor) => (editor.userEmail ?? '').toLowerCase()).filter(Boolean)), + [editors] + ) + + const addEditor = useCallback( + (target: AddPeopleTarget) => upsertEditorAsync({ skillId, workspaceId, userId: target.userId }), + [upsertEditorAsync, skillId, workspaceId] + ) + + const removeEditor = useCallback( + async (userId: string) => { + try { + await removeEditorAsync({ skillId, workspaceId, userId }) + } catch (error) { + logger.error('Failed to remove skill editor', error) + } + }, + [removeEditorAsync, skillId, workspaceId] + ) + + return { + editors, + editorsLoading, + editorsError, + existingEditorEmails, + addEditor, + removeEditor, + } +} diff --git a/apps/sim/app/workspace/[workspaceId]/skills/components/skill-modal/skill-modal.tsx b/apps/sim/app/workspace/[workspaceId]/skills/components/skill-modal/skill-modal.tsx index a8b6e2fb4d..cd6d8d29f7 100644 --- a/apps/sim/app/workspace/[workspaceId]/skills/components/skill-modal/skill-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/skills/components/skill-modal/skill-modal.tsx @@ -12,10 +12,15 @@ import { chipFieldSurfaceClass, cn, } from '@sim/emcn' +import { getErrorMessage } from '@sim/utils/errors' import dynamic from 'next/dynamic' import { useParams } from 'next/navigation' import { SkillImport } from '@/app/workspace/[workspaceId]/skills/components/skill-import' -import { parseSkillMarkdown } from '@/app/workspace/[workspaceId]/skills/components/utils' +import { + isSkillNameConflictError, + parseSkillMarkdown, + validateSkillName, +} from '@/app/workspace/[workspaceId]/skills/components/utils' import type { SkillDefinition } from '@/hooks/queries/skills' import { useCreateSkill, useUpdateSkill } from '@/hooks/queries/skills' @@ -34,12 +39,9 @@ interface SkillModalProps { open: boolean onOpenChange: (open: boolean) => void onSave: () => void - onDelete?: (skillId: string) => void initialValues?: SkillDefinition } -const KEBAB_CASE_REGEX = /^[a-z0-9]+(-[a-z0-9]+)*$/ - interface FieldErrors { name?: string description?: string @@ -49,13 +51,12 @@ interface FieldErrors { type TabValue = 'create' | 'import' -export function SkillModal({ - open, - onOpenChange, - onSave, - onDelete, - initialValues, -}: SkillModalProps) { +const CREATE_TABS = [ + { value: 'create', label: 'Create' }, + { value: 'import', label: 'Import' }, +] as const + +export function SkillModal({ open, onOpenChange, onSave, initialValues }: SkillModalProps) { const params = useParams() const workspaceId = params.workspaceId as string @@ -98,13 +99,8 @@ export function SkillModal({ const handleSave = async () => { const newErrors: FieldErrors = {} - if (!name.trim()) { - newErrors.name = 'Name is required' - } else if (name.length > 64) { - newErrors.name = 'Name must be 64 characters or less' - } else if (!KEBAB_CASE_REGEX.test(name)) { - newErrors.name = 'Name must be kebab-case (e.g. my-skill)' - } + const nameError = validateSkillName(name) + if (nameError) newErrors.name = nameError if (!description.trim()) { newErrors.description = 'Description is required' @@ -136,11 +132,11 @@ export function SkillModal({ } onSave() } catch (error) { - const message = - error instanceof Error && error.message.includes('already exists') - ? error.message - : 'Failed to save skill. Please try again.' - setErrors({ general: message }) + if (isSkillNameConflictError(error)) { + setErrors({ name: getErrorMessage(error, 'This skill name is already taken.') }) + } else { + setErrors({ general: 'Failed to save skill. Please try again.' }) + } } finally { setSaving(false) } @@ -172,8 +168,11 @@ export function SkillModal({ } const isEditing = !!initialValues - const readOnly = !!initialValues?.readOnly - const showFooter = activeTab === 'create' || isEditing + const isBuiltin = !!initialValues?.readOnly + /** New skills are created by the actor (who becomes an editor); existing ones require editor access. */ + const canEditSkill = !initialValues || initialValues.canEdit + const readOnly = isBuiltin || (isEditing && !canEditSkill) + const showFooter = activeTab === 'create' return ( {!isEditing && ( setActiveTab(value as TabValue)} /> @@ -213,6 +209,7 @@ export function SkillModal({ required error={errors.name} hint='Lowercase letters, numbers, and hyphens (e.g. my-skill)' + disabled={readOnly || saving} /> @@ -241,6 +239,7 @@ export function SkillModal({ }} placeholder='Skill instructions in markdown...' minHeight={200} + maxHeight={360} disabled={readOnly || saving} error={!!errors.content} workspaceId={workspaceId} @@ -258,19 +257,7 @@ export function SkillModal({ {showFooter && ( onOpenChange(false)} - cancelDisabled={readOnly} - secondaryActions={ - isEditing && onDelete - ? [ - { - label: 'Delete', - onClick: () => onDelete(initialValues.id), - variant: 'destructive', - disabled: readOnly, - }, - ] - : undefined - } + cancelDisabled={isBuiltin} primaryAction={{ label: saving ? 'Saving...' : isEditing ? 'Update' : 'Create', onClick: handleSave, diff --git a/apps/sim/app/workspace/[workspaceId]/skills/components/utils.ts b/apps/sim/app/workspace/[workspaceId]/skills/components/utils.ts index debefcf690..bdb862bf93 100644 --- a/apps/sim/app/workspace/[workspaceId]/skills/components/utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/skills/components/utils.ts @@ -1,6 +1,7 @@ import JSZip from 'jszip' +import { isApiClientError } from '@/lib/api/client/errors' -interface ParsedSkill { +export interface ParsedSkill { name: string description: string content: string @@ -86,7 +87,6 @@ function inferNameFromHeading(markdown: string): string { /** * Extracts the SKILL.md content from a ZIP archive. * Searches for a file named SKILL.md at any depth within the archive. - * Accepts File, Blob, ArrayBuffer, or Uint8Array (anything JSZip supports). */ export async function extractSkillFromZip( data: File | Blob | ArrayBuffer | Uint8Array @@ -113,3 +113,56 @@ export async function extractSkillFromZip( const content = await zip.file(candidates[0])!.async('string') return content } +const KEBAB_CASE_REGEX = /^[a-z0-9]+(-[a-z0-9]+)*$/ + +/** + * Validates a skill name against the same rules the API enforces + * (`skillNameSchema`). Returns the field message, or null when valid. Shared so + * the rule and its copy live in one place across every skill editing surface. + */ +export function validateSkillName(name: string): string | null { + if (!name.trim()) return 'Name is required' + if (name.length > 64) return 'Name must be 64 characters or less' + if (!KEBAB_CASE_REGEX.test(name)) return 'Name must be kebab-case (e.g. my-skill)' + return null +} + +const SKILL_IMPORT_EXTENSIONS = ['.md', '.zip'] as const + +/** ZIPs are read fully in memory to find the SKILL.md, so cap what we'll accept. */ +const MAX_SKILL_ZIP_BYTES = 5 * 1024 * 1024 + +/** `accept` attribute for the skill file pickers. */ +export const SKILL_IMPORT_ACCEPT = SKILL_IMPORT_EXTENSIONS.join(',') + +/** + * Reads a user-picked `.md` or `.zip` into structured skill fields — the shared + * path behind every import entry point (the create page's Import action and the + * canvas modal's Import tab). Throws a user-facing message on an unsupported + * extension, an oversized ZIP, or a ZIP with no SKILL.md inside. + */ +export async function readSkillFile(file: File): Promise { + const name = file.name.toLowerCase() + + if (!SKILL_IMPORT_EXTENSIONS.some((ext) => name.endsWith(ext))) { + throw new Error('Unsupported file type. Use .md or .zip files.') + } + + if (name.endsWith('.zip')) { + if (file.size > MAX_SKILL_ZIP_BYTES) { + throw new Error('ZIP file is too large (max 5 MB)') + } + return parseSkillMarkdown(await extractSkillFromZip(file)) + } + + return parseSkillMarkdown(await file.text()) +} + +/** + * Whether a skill save failed on the per-workspace unique-name constraint + * (HTTP 409). Surfaces as an inline Name-field error at the callsites; the + * server message names the conflicting skill. + */ +export function isSkillNameConflictError(error: unknown): boolean { + return isApiClientError(error) && error.status === 409 +} diff --git a/apps/sim/app/workspace/[workspaceId]/skills/new/page.tsx b/apps/sim/app/workspace/[workspaceId]/skills/new/page.tsx new file mode 100644 index 0000000000..e8d291584a --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/skills/new/page.tsx @@ -0,0 +1,15 @@ +import type { Metadata } from 'next' +import { SkillCreate } from '@/app/workspace/[workspaceId]/skills/new/skill-create' + +export const metadata: Metadata = { + title: 'New Skill', +} + +export default async function SkillCreatePage({ + params, +}: { + params: Promise<{ workspaceId: string }> +}) { + const { workspaceId } = await params + return +} diff --git a/apps/sim/app/workspace/[workspaceId]/skills/new/skill-create.tsx b/apps/sim/app/workspace/[workspaceId]/skills/new/skill-create.tsx new file mode 100644 index 0000000000..6c4a246f47 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/skills/new/skill-create.tsx @@ -0,0 +1,237 @@ +'use client' + +import { useState } from 'react' +import { + Chip, + ChipInput, + ChipLink, + ChipTextarea, + chipFieldSurfaceClass, + cn, + toast, +} from '@sim/emcn' +import { ArrowLeft } from '@sim/emcn/icons' +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import dynamic from 'next/dynamic' +import { useRouter } from 'next/navigation' +import { SkillTile } from '@/app/workspace/[workspaceId]/components' +import { + CredentialDetailHeading, + CredentialDetailLayout, + DetailSection, + UnsavedChangesModal, + useUnsavedChangesGuard, +} from '@/app/workspace/[workspaceId]/components/credential-detail' +import { SkillImportButton } from '@/app/workspace/[workspaceId]/skills/components/skill-import' +import { + isSkillNameConflictError, + type ParsedSkill, + parseSkillMarkdown, + validateSkillName, +} from '@/app/workspace/[workspaceId]/skills/components/utils' +import { useCreateSkill } from '@/hooks/queries/skills' + +const RichMarkdownField = dynamic( + () => + import( + '@/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-field' + ).then((m) => m.RichMarkdownField), + { + ssr: false, + loading: () =>
, + } +) + +const logger = createLogger('SkillCreate') + +interface FieldErrors { + name?: string + description?: string + content?: string +} + +interface SkillCreateProps { + workspaceId: string +} + +/** + * Full-page skill creation, mirroring the skill detail surface: a fixed action + * bar (Import / Create skill), a heading, and the editable Name / Description / + * Content sections. Importing a SKILL.md prefills all three fields in place. + */ +export function SkillCreate({ workspaceId }: SkillCreateProps) { + const router = useRouter() + const skillsHref = `/workspace/${workspaceId}/skills` + + const createSkill = useCreateSkill() + + const [nameDraft, setNameDraft] = useState('') + const [descriptionDraft, setDescriptionDraft] = useState('') + const [contentDraft, setContentDraft] = useState('') + /** Bumped to remount the seed-once rich Content editor on programmatic sets. */ + const [contentSeed, setContentSeed] = useState(0) + const [errors, setErrors] = useState({}) + + // Drops on success so the guard pops its history sentinel before we navigate — + // otherwise Back from the new skill lands on a stale, empty create form. + const isDirty = + !createSkill.isSuccess && + (!!nameDraft.trim() || !!descriptionDraft.trim() || !!contentDraft.trim()) + + const guard = useUnsavedChangesGuard({ isDirty, backHref: skillsHref }) + + const handleCreate = async () => { + if (createSkill.isPending) return + + const newErrors: FieldErrors = {} + const nameError = validateSkillName(nameDraft) + if (nameError) newErrors.name = nameError + if (!descriptionDraft.trim()) newErrors.description = 'Description is required' + if (!contentDraft.trim()) newErrors.content = 'Content is required' + if (Object.keys(newErrors).length > 0) { + setErrors(newErrors) + return + } + + try { + const created = await createSkill.mutateAsync({ + workspaceId, + skill: { name: nameDraft, description: descriptionDraft, content: contentDraft }, + }) + setErrors({}) + // The upsert responds with the caller's whole skill list (built-ins + // included), not just the new row — match by name, which is unique per + // workspace, rather than trusting the first element. + const createdId = created.find((skill) => skill.name === nameDraft)?.id + router.push(createdId ? `${skillsHref}/${createdId}` : skillsHref) + } catch (error) { + if (isSkillNameConflictError(error)) { + setErrors({ name: getErrorMessage(error, 'This skill name is already taken.') }) + } else { + toast.error("Couldn't create skill", { + description: getErrorMessage(error, 'Please try again in a moment.'), + }) + } + logger.error('Failed to create skill', error) + } + } + + const applyImportedSkill = (data: ParsedSkill) => { + setNameDraft(data.name) + setDescriptionDraft(data.description) + setContentDraft(data.content) + setErrors({}) + setContentSeed((seed) => seed + 1) + } + + /** + * Pasting a full SKILL.md destructures it into the fields. Gated on a real + * YAML `name:` key so a stray `---` or heading-only snippet pastes as + * ordinary content instead of silently overwriting all three fields. + */ + const handleContentPaste = (text: string): boolean => { + const parsed = parseSkillMarkdown(text) + if (!parsed.nameFromFrontmatter) return false + applyImportedSkill(parsed) + return true + } + + const back = ( + + Skills + + ) + + const actions = ( + <> + + + {createSkill.isPending ? 'Creating...' : 'Create'} + + + ) + + return ( + <> + + } + title='New skill' + subtitle='Write a skill, or import an existing SKILL.md' + /> + + + { + setNameDraft(event.target.value) + if (errors.name) setErrors((prev) => ({ ...prev, name: undefined })) + }} + placeholder='my-skill-name' + autoComplete='off' + data-lpignore='true' + disabled={createSkill.isPending} + error={!!errors.name} + /> +

+ {errors.name ?? 'Lowercase letters, numbers, and hyphens (e.g. my-skill)'} +

+
+ + + { + setDescriptionDraft(event.target.value) + if (errors.description) setErrors((prev) => ({ ...prev, description: undefined })) + }} + placeholder='What this skill does and when to use it...' + maxLength={1024} + autoComplete='off' + data-lpignore='true' + disabled={createSkill.isPending} + error={!!errors.description} + /> + {errors.description && ( +

{errors.description}

+ )} +
+ + + { + setContentDraft(value) + if (errors.content) setErrors((prev) => ({ ...prev, content: undefined })) + }} + placeholder='Skill instructions in markdown...' + minHeight={260} + disabled={createSkill.isPending} + error={!!errors.content} + workspaceId={workspaceId} + onPasteText={handleContentPaste} + /> + {errors.content && ( +

{errors.content}

+ )} +
+
+ + + + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/skills/search-params.ts b/apps/sim/app/workspace/[workspaceId]/skills/search-params.ts index 554005fc76..e78b8167fc 100644 --- a/apps/sim/app/workspace/[workspaceId]/skills/search-params.ts +++ b/apps/sim/app/workspace/[workspaceId]/skills/search-params.ts @@ -3,20 +3,18 @@ import { parseAsString } from 'nuqs/server' /** * Co-located, typed URL query-param definition for the Skills gallery. * - * `skillId` deep-links the skill edit modal to a specific skill. The modal - * opens when a valid id (one that resolves to a loaded skill) is present; - * closing it clears the param. Opening a skill is a destination, so it lands in - * the browser history (`history: 'push'`). The "create new skill" flow has no id - * and stays in local component state. + * `skillId` is a LEGACY deep-link param from when skills edited in a modal on + * this page. Skills now open at `/workspace/[workspaceId]/skills/[skillId]`; + * the gallery reads this param once and redirects there (read-then-strip). */ export const skillIdParam = { key: 'skillId', parser: parseAsString, } as const -/** Opening a skill is a destination → push to history; clear on close. */ +/** Read-once redirect signal — replace history, never linger. */ export const skillIdUrlKeys = { - history: 'push', + history: 'replace', clearOnDefault: true, } as const diff --git a/apps/sim/app/workspace/[workspaceId]/skills/skills.tsx b/apps/sim/app/workspace/[workspaceId]/skills/skills.tsx index d045bf88b1..7004a6c3eb 100644 --- a/apps/sim/app/workspace/[workspaceId]/skills/skills.tsx +++ b/apps/sim/app/workspace/[workspaceId]/skills/skills.tsx @@ -1,27 +1,23 @@ 'use client' -import { useState } from 'react' -import { Chip, ChipConfirmModal, ChipInput, Search } from '@sim/emcn' -import { createLogger } from '@sim/logger' +import { useEffect, useRef } from 'react' +import { Chip, ChipInput, Search } from '@sim/emcn' import { getErrorMessage } from '@sim/utils/errors' import { ArrowRight, Plus } from 'lucide-react' -import { useParams } from 'next/navigation' +import { useParams, useRouter } from 'next/navigation' import { useQueryState } from 'nuqs' import { SkillTile } from '@/app/workspace/[workspaceId]/components' import { IntegrationTabsHeader } from '@/app/workspace/[workspaceId]/integrations/components/integration-tabs-header' import { ShowcaseWithExplore } from '@/app/workspace/[workspaceId]/integrations/components/showcase-with-explore' -import { SkillModal } from '@/app/workspace/[workspaceId]/skills/components/skill-modal' import { skillIdParam, skillIdUrlKeys, skillSearchParam, skillSearchUrlKeys, } from '@/app/workspace/[workspaceId]/skills/search-params' -import { useDeleteSkill, useSkills } from '@/hooks/queries/skills' +import { useSkills } from '@/hooks/queries/skills' import { useDebouncedSearchSetter } from '@/hooks/use-debounced-search-setter' -const logger = createLogger('SkillsSettings') - const SKILLS_LABEL = 'Skills' interface SkillItemProps { @@ -68,22 +64,31 @@ function SkillSection({ label, children }: SkillSectionProps) { export function Skills() { const params = useParams() + const router = useRouter() const workspaceId = (params?.workspaceId as string) || '' + const skillsHref = `/workspace/${workspaceId}/skills` const { data: skills = [], isLoading, error } = useSkills(workspaceId) - const deleteSkillMutation = useDeleteSkill() const [searchTerm, setSearchTermParam] = useQueryState(skillSearchParam.key, { ...skillSearchParam.parser, ...skillSearchUrlKeys, }) - const [editingSkillId, setEditingSkillId] = useQueryState(skillIdParam.key, { + const [legacySkillId, setLegacySkillId] = useQueryState(skillIdParam.key, { ...skillIdParam.parser, ...skillIdUrlKeys, }) - const [showAddForm, setShowAddForm] = useState(false) - const [skillToDelete, setSkillToDelete] = useState<{ id: string; name: string } | null>(null) - const [showDeleteDialog, setShowDeleteDialog] = useState(false) + /** + * Legacy deep links opened the edit modal via `?skillId=`; skills now have a + * dedicated detail page. Redirect once, stripping the param. + */ + const redirectedLegacyId = useRef(false) + useEffect(() => { + if (!legacySkillId || redirectedLegacyId.current) return + redirectedLegacyId.current = true + setLegacySkillId(null, { history: 'replace' }) + router.replace(`${skillsHref}/${legacySkillId}`) + }, [legacySkillId, setLegacySkillId, router, skillsHref]) /** * The input is controlled directly by the instant nuqs value; only the URL @@ -92,9 +97,6 @@ export function Skills() { */ const setSearchTerm = useDebouncedSearchSetter(setSearchTermParam) - /** Derive the skill being edited from the loaded list — never store the object in the URL. */ - const editingSkill = editingSkillId ? (skills.find((s) => s.id === editingSkillId) ?? null) : null - const filteredSkills = skills.filter((s) => { if (!searchTerm.trim()) return true const searchLower = searchTerm.trim().toLowerCase() @@ -104,46 +106,15 @@ export function Skills() { ) }) - const handleDeleteClick = (skillId: string) => { - const s = skills.find((sk) => sk.id === skillId) - if (!s) return - - setSkillToDelete({ id: skillId, name: s.name }) - setShowDeleteDialog(true) - } - - const handleDeleteSkill = async () => { - if (!skillToDelete) return - - setShowDeleteDialog(false) - - try { - await deleteSkillMutation.mutateAsync({ - workspaceId, - skillId: skillToDelete.id, - }) - logger.info(`Deleted skill: ${skillToDelete.id}`) - } catch (error) { - logger.error('Error deleting skill:', error) - } finally { - setSkillToDelete(null) - } - } - - const handleSkillSaved = () => { - setShowAddForm(false) - setEditingSkillId(null) - } - const showNoResults = searchTerm.trim() && filteredSkills.length === 0 - const handleOpenAddForm = () => { - setEditingSkillId(null) - setShowAddForm(true) - } - const addButton = ( - + router.push(`${skillsHref}/new`)} + disabled={isLoading} + leftIcon={Plus} + > Add to Sim ) @@ -177,7 +148,7 @@ export function Skills() { key={s.id} name={s.name} description={s.description} - onClick={() => setEditingSkillId(s.id)} + onClick={() => router.push(`${skillsHref}/${s.id}`)} /> ))} @@ -189,38 +160,6 @@ export function Skills() {
- - { - if (!open) { - setShowAddForm(false) - setEditingSkillId(null) - } - }} - onSave={handleSkillSaved} - onDelete={(skillId) => { - setEditingSkillId(null) - handleDeleteClick(skillId) - }} - initialValues={editingSkill ?? undefined} - /> - -
) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/skill-input/skill-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/skill-input/skill-input.tsx index 80be87af9e..ac2282c5b6 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/skill-input/skill-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/skill-input/skill-input.tsx @@ -10,8 +10,7 @@ import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/ import { getWorkflowSearchLabelHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' import { useActiveSearchTarget } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider' -import type { SkillDefinition } from '@/hooks/queries/skills' -import { useSkills } from '@/hooks/queries/skills' +import { type SkillDefinition, useSkills } from '@/hooks/queries/skills' import { usePermissionConfig } from '@/hooks/use-permission-config' interface StoredSkill { @@ -42,7 +41,16 @@ export function SkillInput({ const { data: workspaceSkills = [] } = useSkills(workspaceId) const [value, setValue] = useSubBlockValue(blockId, subBlockId) const [showCreateModal, setShowCreateModal] = useState(false) - const [editingSkill, setEditingSkill] = useState(null) + const [editingSkillId, setEditingSkillId] = useState(null) + const [editingSkillSnapshot, setEditingSkillSnapshot] = useState(null) + + // Prefer the live query cache so the modal reflects concurrent edits, but + // fall back to the click-time snapshot when a background refetch drops the + // skill — otherwise the modal would close mid-edit and silently discard the + // draft; saving surfaces the real server error instead. + const editingSkill = editingSkillId + ? (workspaceSkills.find((s) => s.id === editingSkillId) ?? editingSkillSnapshot) + : null const selectedSkills: StoredSkill[] = useMemo(() => { if (isPreview && previewValue) { @@ -105,7 +113,8 @@ export function SkillInput({ const handleSkillSaved = useCallback(() => { setShowCreateModal(false) - setEditingSkill(null) + setEditingSkillId(null) + setEditingSkillSnapshot(null) }, []) const resolveSkillName = useCallback( @@ -150,7 +159,8 @@ export function SkillInput({ className='flex cursor-pointer items-center justify-between gap-2 rounded-t-[4px] bg-[var(--surface-4)] px-2 py-[6.5px]' onClick={() => { if (fullSkill && !disabled && !isPreview) { - setEditingSkill(fullSkill) + setEditingSkillId(fullSkill.id) + setEditingSkillSnapshot(fullSkill) } }} > @@ -188,7 +198,8 @@ export function SkillInput({ onOpenChange={(isOpen) => { if (!isOpen) { setShowCreateModal(false) - setEditingSkill(null) + setEditingSkillId(null) + setEditingSkillSnapshot(null) } }} onSave={handleSkillSaved} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/code-editor/code-editor.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/code-editor/code-editor.tsx index a6df56db16..61012a92c3 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/code-editor/code-editor.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/code-editor/code-editor.tsx @@ -4,14 +4,14 @@ import { CODE_LINE_HEIGHT_PX, Code, calculateGutterWidth, + chipFieldSurfaceClass, cn, getCodeEditorProps, highlight, languages, } from '@sim/emcn' -import { Wand2 } from 'lucide-react' import Editor from 'react-simple-code-editor' -import { Button } from '@/components/ui/button' +import type { SchemaParameter } from '@/app/workspace/[workspaceId]/components/custom-tool-editor/custom-tool-schema' import { createEnvVarPattern, createWorkflowVariablePattern, @@ -22,16 +22,15 @@ interface CodeEditorProps { onChange: (value: string) => void language: 'javascript' | 'json' placeholder?: string + /** Layout/sizing only — the chip-field chrome is owned by this component. */ className?: string - gutterClassName?: string + /** Swaps the field border to the error token. */ + error?: boolean minHeight?: string highlightVariables?: boolean onKeyDown?: (e: React.KeyboardEvent) => void disabled?: boolean - schemaParameters?: Array<{ name: string; type: string; description: string; required: boolean }> - showWandButton?: boolean - onWandClick?: () => void - wandButtonDisabled?: boolean + schemaParameters?: SchemaParameter[] } const EMPTY_SCHEMA_PARAMETERS: NonNullable = [] @@ -42,15 +41,12 @@ export function CodeEditor({ language, placeholder = '', className = '', - gutterClassName = '', + error = false, minHeight, highlightVariables = true, onKeyDown, disabled = false, schemaParameters = EMPTY_SCHEMA_PARAMETERS, - showWandButton = false, - onWandClick, - wandButtonDisabled = false, }: CodeEditorProps) { const [visualLineHeights, setVisualLineHeights] = useState([]) @@ -182,21 +178,11 @@ export function CodeEditor({ } return ( - - {showWandButton && onWandClick && ( - - )} - - + + {renderLineNumbers()} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/custom-tool-modal/custom-tool-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/custom-tool-modal/custom-tool-modal.tsx index ab399baa0d..10dc434c4a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/custom-tool-modal/custom-tool-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/custom-tool-modal/custom-tool-modal.tsx @@ -1,37 +1,30 @@ -import { useEffect, useMemo, useRef, useState } from 'react' +'use client' + +import { useEffect, useMemo, useState } from 'react' import { - Badge, - Button, ChipConfirmModal, ChipModal, ChipModalBody, ChipModalFooter, ChipModalHeader, ChipModalTabs, - cn, - Input, - Label, - Popover, - PopoverAnchor, - PopoverContent, - PopoverItem, - PopoverScrollArea, - PopoverSection, + toast, } from '@sim/emcn' import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' -import { AlertCircle, ArrowUp } from 'lucide-react' import { useParams } from 'next/navigation' import { - checkEnvVarTrigger, - EnvVarDropdown, -} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/env-var-dropdown' -import { - checkTagTrigger, - TagDropdown, -} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown' -import { CodeEditor } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/code-editor/code-editor' -import { useWand } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-wand' + CUSTOM_TOOL_DELETE_CONFIRM_TEXT, + CustomToolCodeField, + CustomToolSchemaField, + extractSchemaParameters, + FieldErrorText, + FUNCTION_NAME_LOCKED, + GeneratePromptControl, + useCodeGeneration, + useSchemaGeneration, + validateCustomToolSchema, +} from '@/app/workspace/[workspaceId]/components/custom-tool-editor' import { useCreateCustomTool, useCustomTools, @@ -68,6 +61,11 @@ export interface CustomTool { type ToolSection = 'schema' | 'code' +const TOOL_TABS = [ + { value: 'schema', label: 'Schema' }, + { value: 'code', label: 'Code' }, +] as const + export function CustomToolModal({ open, onOpenChange, @@ -89,216 +87,34 @@ export function CustomToolModal({ const [initialFunctionCode, setInitialFunctionCode] = useState('') const [showDeleteConfirm, setShowDeleteConfirm] = useState(false) const [showDiscardAlert, setShowDiscardAlert] = useState(false) - const [isSchemaPromptActive, setIsSchemaPromptActive] = useState(false) - const [schemaPromptInput, setSchemaPromptInput] = useState('') - const schemaPromptInputRef = useRef(null) - const [isCodePromptActive, setIsCodePromptActive] = useState(false) - const [codePromptInput, setCodePromptInput] = useState('') - const codePromptInputRef = useRef(null) + const schemaParameters = useMemo(() => extractSchemaParameters(jsonSchema), [jsonSchema]) - const schemaGeneration = useWand({ - wandConfig: { - enabled: true, - maintainHistory: true, - prompt: `You are an expert programmer specializing in creating OpenAI function calling format JSON schemas for custom tools. -Generate ONLY the JSON schema based on the user's request. -The output MUST be a single, valid JSON object, starting with { and ending with }. -The JSON schema MUST follow this specific format: -1. Top-level property "type" must be set to "function" -2. A "function" object containing: - - "name": A concise, camelCase name for the function - - "description": A clear description of what the function does - - "parameters": A JSON Schema object describing the function's parameters with: - - "type": "object" - - "properties": An object containing parameter definitions - - "required": An array of required parameter names - -Current schema: {context} - -Do not include any explanations, markdown formatting, or other text outside the JSON object. - -Valid Schema Examples: - -Example 1: -{ - "type": "function", - "function": { - "name": "getWeather", - "description": "Fetches the current weather for a specific location.", - "parameters": { - "type": "object", - "properties": { - "location": { - "type": "string", - "description": "The city and state, e.g., San Francisco, CA" - }, - "unit": { - "type": "string", - "description": "Temperature unit", - "enum": ["celsius", "fahrenheit"] - } - }, - "required": ["location"], - "additionalProperties": false - } - } -} - -Example 2: -{ - "type": "function", - "function": { - "name": "addItemToOrder", - "description": "Add one quantity of a food item to the order.", - "parameters": { - "type": "object", - "properties": { - "itemName": { - "type": "string", - "description": "The name of the food item to add to order" - }, - "quantity": { - "type": "integer", - "description": "The quantity of the item to add", - "default": 1 - } - }, - "required": ["itemName"], - "additionalProperties": false - } - } -}`, - placeholder: 'Describe the function parameters and structure...', - generationType: 'custom-tool-schema', - }, - currentValue: jsonSchema, - onStreamStart: () => { - setJsonSchema('') - }, - onGeneratedContent: (content) => { - setJsonSchema(content) + const schemaGeneration = useSchemaGeneration({ + jsonSchema, + setJsonSchema: (updater) => { + setJsonSchema(updater) setSchemaError(null) }, - onStreamChunk: (chunk) => { - setJsonSchema((prev) => { - const newSchema = prev + chunk - if (schemaError) setSchemaError(null) - return newSchema - }) + replaceJsonSchema: (value) => { + setJsonSchema(value) + setSchemaError(null) }, }) - const schemaParameters = useMemo(() => { - try { - if (!jsonSchema) return [] - const parsed = JSON.parse(jsonSchema) - const properties = parsed?.function?.parameters?.properties - if (!properties) return [] - - return Object.keys(properties).map((key) => ({ - name: key, - type: properties[key].type || 'any', - description: properties[key].description || '', - required: parsed?.function?.parameters?.required?.includes(key) || false, - })) - } catch { - return [] - } - }, [jsonSchema]) - - const codeGenerationSchemaContext = useMemo(() => { - if (schemaParameters.length === 0) { - return 'Schema parameters: (none defined yet — the user has not added any parameters to the schema)' - } - const lines = schemaParameters.map((p) => { - const requiredLabel = p.required ? 'required' : 'optional' - const description = p.description ? `: ${p.description}` : '' - return `- ${p.name} (${p.type}, ${requiredLabel})${description}` - }) - return `Schema parameters (reference these directly by name in the generated code):\n${lines.join('\n')}` - }, [schemaParameters]) - - const codeGeneration = useWand({ - wandConfig: { - enabled: true, - maintainHistory: true, - prompt: `You are an expert JavaScript programmer. -Generate ONLY the raw body of a JavaScript function based on the user's request. -The code should be executable within an 'async function(params, environmentVariables) {...}' context. -- 'params' (object): Contains input parameters derived from the JSON schema. Reference these directly by name (e.g., 'userId', 'cityName'). Do NOT use 'params.paramName'. -- 'environmentVariables' (object): Contains environment variables. Reference these using the double curly brace syntax: '{{ENV_VAR_NAME}}'. Do NOT use 'environmentVariables.VAR_NAME' or env. - -${codeGenerationSchemaContext} - -Current code: {context} - -IMPORTANT FORMATTING RULES: -1. Reference Environment Variables: Use the exact syntax {{VARIABLE_NAME}}. Do NOT wrap it in quotes (e.g., use 'const apiKey = {{SERVICE_API_KEY}};' not 'const apiKey = "{{SERVICE_API_KEY}}";'). Our system replaces these placeholders before execution. -2. Reference Input Parameters/Workflow Variables: Reference them directly by name (e.g., 'const city = cityName;' or use directly in template strings like \`\${cityName}\`). Do NOT wrap in quotes or angle brackets. -3. Function Body ONLY: Do NOT include the function signature (e.g., 'async function myFunction() {' or the surrounding '}'). -4. Imports: Do NOT include import/require statements unless they are standard Node.js built-in modules (e.g., 'crypto', 'fs'). External libraries are not supported in this context. -5. Output: Ensure the code returns a value if the function is expected to produce output. Use 'return'. -6. Clarity: Write clean, readable code. -7. No Explanations: Do NOT include markdown formatting, comments explaining the rules, or any text other than the raw JavaScript code for the function body. - -Example Scenario: -User Prompt: "Fetch weather data from OpenWeather API. Use the city name passed in as 'cityName' and an API Key stored as the 'OPENWEATHER_API_KEY' environment variable." - -Generated Code: -const apiKey = {{OPENWEATHER_API_KEY}}; -const url = \`https://api.openweathermap.org/data/2.5/weather?q=\${cityName}&appid=\${apiKey}\`; - -try { - const response = await fetch(url, { - method: 'GET', - headers: { - 'Content-Type': 'application/json' - } - }); - - if (!response.ok) { - throw new Error(\`API request failed with status \${response.status}: \${await response.text()}\`); - } - - const weatherData = await response.json(); - return weatherData; -} catch (error) { - console.error(\`Error fetching weather data: \${error.message}\`); - throw error; -}`, - placeholder: 'Describe the JavaScript function to generate...', - generationType: 'javascript-function-body', - }, - currentValue: functionCode, - onStreamStart: () => { - setFunctionCode('') - }, - onGeneratedContent: (content) => { - handleFunctionCodeChange(content) + const codeGeneration = useCodeGeneration({ + functionCode, + schemaParameters, + setFunctionCode: (updater) => { + setFunctionCode(updater) setCodeError(null) }, - onStreamChunk: (chunk) => { - setFunctionCode((prev) => { - const newCode = prev + chunk - handleFunctionCodeChange(newCode) - if (codeError) setCodeError(null) - return newCode - }) + replaceFunctionCode: (value) => { + setFunctionCode(value) + setCodeError(null) }, }) - const [showEnvVars, setShowEnvVars] = useState(false) - const [showTags, setShowTags] = useState(false) - const [showSchemaParams, setShowSchemaParams] = useState(false) - const [searchTerm, setSearchTerm] = useState('') - const [cursorPosition, setCursorPosition] = useState(0) - const codeEditorRef = useRef(null) - const [activeSourceBlockId, setActiveSourceBlockId] = useState(null) - const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0 }) - const [schemaParamSelectedIndex, setSchemaParamSelectedIndex] = useState(0) - const schemaParamItemRefs = useRef>(new Map()) - const createToolMutation = useCreateCustomTool() const updateToolMutation = useUpdateCustomTool() const deleteToolMutation = useDeleteCustomTool() @@ -329,18 +145,6 @@ try { } }, [open]) - useEffect(() => { - if (!showSchemaParams || schemaParamSelectedIndex < 0) return - - const element = schemaParamItemRefs.current.get(schemaParamSelectedIndex) - if (element) { - element.scrollIntoView({ - behavior: 'smooth', - block: 'nearest', - }) - } - }, [schemaParamSelectedIndex, showSchemaParams]) - const resetForm = () => { setJsonSchema('') setFunctionCode('') @@ -351,15 +155,7 @@ try { setActiveSection('schema') setIsEditing(false) setToolId(undefined) - setIsSchemaPromptActive(false) - setIsCodePromptActive(false) - setSchemaPromptInput('') - setCodePromptInput('') setShowDiscardAlert(false) - schemaGeneration.closePrompt() - schemaGeneration.hidePromptInline() - codeGeneration.closePrompt() - codeGeneration.hidePromptInline() } const handleClose = () => { @@ -369,53 +165,18 @@ try { onOpenChange(false) } - const validateSchema = (schema: string): { isValid: boolean; error: string | null } => { - if (!schema) return { isValid: false, error: null } + /** The Generate control and error live in the shared tab row, so both follow the visible editor. */ + const activeGeneration = activeSection === 'schema' ? schemaGeneration : codeGeneration + const activeError = + activeSection === 'schema' ? schemaError : codeGeneration.isStreaming ? null : codeError - try { - const parsed = JSON.parse(schema) + const isSchemaValid = useMemo(() => validateCustomToolSchema(jsonSchema).isValid, [jsonSchema]) - if (!parsed.type || parsed.type !== 'function') { - return { isValid: false, error: 'Missing "type": "function"' } - } - if (!parsed.function || !parsed.function.name) { - return { isValid: false, error: 'Missing function.name field' } - } - if (!parsed.function.parameters) { - return { isValid: false, error: 'Missing function.parameters object' } - } - if (!parsed.function.parameters.type) { - return { isValid: false, error: 'Missing parameters.type field' } - } - if (parsed.function.parameters.properties === undefined) { - return { isValid: false, error: 'Missing parameters.properties field' } - } - if ( - typeof parsed.function.parameters.properties !== 'object' || - parsed.function.parameters.properties === null - ) { - return { isValid: false, error: 'parameters.properties must be an object' } - } - - return { isValid: true, error: null } - } catch { - return { isValid: false, error: 'Invalid JSON format' } - } - } - - const isSchemaValid = useMemo(() => validateSchema(jsonSchema).isValid, [jsonSchema]) - - const hasChanges = useMemo(() => { - if (!isEditing) return true - return jsonSchema !== initialJsonSchema || functionCode !== initialFunctionCode - }, [isEditing, jsonSchema, initialJsonSchema, functionCode, initialFunctionCode]) - - const hasUnsavedChanges = useMemo(() => { - if (isEditing) { - return jsonSchema !== initialJsonSchema || functionCode !== initialFunctionCode - } - return jsonSchema.trim().length > 0 || functionCode.trim().length > 0 - }, [isEditing, jsonSchema, initialJsonSchema, functionCode, initialFunctionCode]) + const edited = jsonSchema !== initialJsonSchema || functionCode !== initialFunctionCode + const canSave = !isEditing || edited + const hasUnsavedChanges = isEditing + ? edited + : jsonSchema.trim().length > 0 || functionCode.trim().length > 0 const handleCloseAttempt = () => { if (hasUnsavedChanges && !schemaGeneration.isStreaming && !codeGeneration.isStreaming) { @@ -438,7 +199,7 @@ try { return } - const { isValid, error } = validateSchema(jsonSchema) + const { isValid, error } = validateCustomToolSchema(jsonSchema) if (!isValid) { setSchemaError(error) setActiveSection('schema') @@ -509,9 +270,7 @@ try { const errorMessage = getErrorMessage(error, 'Failed to save custom tool') if (errorMessage.includes('Cannot change function name')) { - setSchemaError( - 'Function name cannot be changed after creation. To use a different name, delete this tool and create a new one.' - ) + setSchemaError(FUNCTION_NAME_LOCKED) } else { setSchemaError(errorMessage) } @@ -520,11 +279,10 @@ try { } const handleJsonSchemaChange = (value: string) => { - if (schemaGeneration.isLoading || schemaGeneration.isStreaming) return setJsonSchema(value) if (value.trim()) { - const { error } = validateSchema(value) + const { error } = validateCustomToolSchema(value) setSchemaError(error) } else { setSchemaError(null) @@ -532,265 +290,8 @@ try { } const handleFunctionCodeChange = (value: string) => { - if (codeGeneration.isLoading || codeGeneration.isStreaming) { - setFunctionCode(value) - if (codeError) { - setCodeError(null) - } - return - } - setFunctionCode(value) - if (codeError) { - setCodeError(null) - } - - const textarea = codeEditorRef.current?.querySelector('textarea') - if (textarea) { - const pos = textarea.selectionStart - setCursorPosition(pos) - - const textBeforeCursor = value.substring(0, pos) - const lines = textBeforeCursor.split('\n') - const currentLine = lines.length - const currentCol = lines[lines.length - 1].length - - try { - if (codeEditorRef.current) { - const editorRect = codeEditorRef.current.getBoundingClientRect() - const lineHeight = 21 - - const top = currentLine * lineHeight + 5 - const left = Math.min(currentCol * 8, editorRect.width - 260) - - setDropdownPosition({ top, left }) - } - } catch (error) { - logger.error('Error calculating cursor position:', { error }) - } - - const envVarTrigger = checkEnvVarTrigger(value, pos) - setShowEnvVars(envVarTrigger.show && !codeGeneration.isStreaming) - setSearchTerm(envVarTrigger.show ? envVarTrigger.searchTerm : '') - - const tagTrigger = checkTagTrigger(value, pos) - setShowTags(tagTrigger.show && !codeGeneration.isStreaming) - if (!tagTrigger.show) { - setActiveSourceBlockId(null) - } - - if (!codeGeneration.isStreaming && schemaParameters.length > 0) { - const schemaParamTrigger = checkSchemaParamTrigger(value, pos, schemaParameters) - if (schemaParamTrigger.show && !showSchemaParams) { - setShowSchemaParams(true) - setSchemaParamSelectedIndex(0) - } else if (!schemaParamTrigger.show && showSchemaParams) { - setShowSchemaParams(false) - } - } - } - } - - const checkSchemaParamTrigger = (text: string, cursorPos: number, parameters: any[]) => { - if (parameters.length === 0) return { show: false, searchTerm: '' } - - const beforeCursor = text.substring(0, cursorPos) - const words = beforeCursor.split(/[\s=();,{}[\]]+/) - const currentWord = words[words.length - 1] || '' - - if (currentWord.length > 0 && /^[a-zA-Z_][\w]*$/.test(currentWord)) { - const matchingParams = parameters.filter((param) => - param.name.toLowerCase().startsWith(currentWord.toLowerCase()) - ) - return { show: matchingParams.length > 0, searchTerm: currentWord, matches: matchingParams } - } - - return { show: false, searchTerm: '' } - } - - const handleEnvVarSelect = (newValue: string) => { - setFunctionCode(newValue) - setShowEnvVars(false) - } - - const handleTagSelect = (newValue: string) => { - setFunctionCode(newValue) - setShowTags(false) - setActiveSourceBlockId(null) - } - - const handleSchemaParamSelect = (paramName: string) => { - const textarea = codeEditorRef.current?.querySelector('textarea') - if (textarea) { - const pos = textarea.selectionStart - const beforeCursor = functionCode.substring(0, pos) - const afterCursor = functionCode.substring(pos) - - const words = beforeCursor.split(/[\s=();,{}[\]]+/) - const currentWord = words[words.length - 1] || '' - const wordStart = beforeCursor.lastIndexOf(currentWord) - - const newValue = beforeCursor.substring(0, wordStart) + paramName + afterCursor - setFunctionCode(newValue) - setShowSchemaParams(false) - - setTimeout(() => { - textarea.focus() - textarea.setSelectionRange(wordStart + paramName.length, wordStart + paramName.length) - }, 0) - } - } - - const handleKeyDown = (e: React.KeyboardEvent) => { - const isSchemaPromptVisible = activeSection === 'schema' && schemaGeneration.isPromptVisible - const isCodePromptVisible = activeSection === 'code' && codeGeneration.isPromptVisible - - if (e.key === 'Escape') { - if (isSchemaPromptVisible) { - schemaGeneration.hidePromptInline() - e.preventDefault() - e.stopPropagation() - return - } - if (isCodePromptVisible) { - codeGeneration.hidePromptInline() - e.preventDefault() - e.stopPropagation() - return - } - if (showEnvVars || showTags || showSchemaParams) { - setShowEnvVars(false) - setShowTags(false) - setShowSchemaParams(false) - e.preventDefault() - e.stopPropagation() - return - } - } - - if (activeSection === 'schema' && schemaGeneration.isStreaming) { - e.preventDefault() - return - } - if (activeSection === 'code' && codeGeneration.isStreaming) { - e.preventDefault() - return - } - - if (showSchemaParams && schemaParameters.length > 0) { - switch (e.key) { - case 'ArrowDown': - e.preventDefault() - e.stopPropagation() - setSchemaParamSelectedIndex((prev) => Math.min(prev + 1, schemaParameters.length - 1)) - return - case 'ArrowUp': - e.preventDefault() - e.stopPropagation() - setSchemaParamSelectedIndex((prev) => Math.max(prev - 1, 0)) - return - case 'Enter': - e.preventDefault() - e.stopPropagation() - if (schemaParamSelectedIndex >= 0 && schemaParamSelectedIndex < schemaParameters.length) { - const selectedParam = schemaParameters[schemaParamSelectedIndex] - handleSchemaParamSelect(selectedParam.name) - } - return - case 'Escape': - e.preventDefault() - e.stopPropagation() - setShowSchemaParams(false) - return - case ' ': - case 'Tab': - setShowSchemaParams(false) - return - } - } - - if (showEnvVars || showTags) { - if (['ArrowDown', 'ArrowUp', 'Enter'].includes(e.key)) { - e.preventDefault() - e.stopPropagation() - } - } - } - - const handleSchemaWandClick = () => { - if (schemaGeneration.isLoading || schemaGeneration.isStreaming) return - setIsSchemaPromptActive(true) - setSchemaPromptInput('') - setTimeout(() => { - schemaPromptInputRef.current?.focus() - }, 0) - } - - const handleSchemaPromptBlur = () => { - if (!schemaPromptInput.trim() && !schemaGeneration.isStreaming) { - setIsSchemaPromptActive(false) - } - } - - const handleSchemaPromptChange = (value: string) => { - setSchemaPromptInput(value) - } - - const handleSchemaPromptSubmit = () => { - const trimmedPrompt = schemaPromptInput.trim() - if (!trimmedPrompt || schemaGeneration.isLoading || schemaGeneration.isStreaming) return - schemaGeneration.generateStream({ prompt: trimmedPrompt }) - setSchemaPromptInput('') - setIsSchemaPromptActive(false) - } - - const handleSchemaPromptKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - e.preventDefault() - handleSchemaPromptSubmit() - } else if (e.key === 'Escape') { - e.preventDefault() - setSchemaPromptInput('') - setIsSchemaPromptActive(false) - } - } - - const handleCodeWandClick = () => { - if (codeGeneration.isLoading || codeGeneration.isStreaming) return - setIsCodePromptActive(true) - setCodePromptInput('') - setTimeout(() => { - codePromptInputRef.current?.focus() - }, 0) - } - - const handleCodePromptBlur = () => { - if (!codePromptInput.trim() && !codeGeneration.isStreaming) { - setIsCodePromptActive(false) - } - } - - const handleCodePromptChange = (value: string) => { - setCodePromptInput(value) - } - - const handleCodePromptSubmit = () => { - const trimmedPrompt = codePromptInput.trim() - if (!trimmedPrompt || codeGeneration.isLoading || codeGeneration.isStreaming) return - codeGeneration.generateStream({ prompt: trimmedPrompt }) - setCodePromptInput('') - setIsCodePromptActive(false) - } - - const handleCodePromptKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - e.preventDefault() - handleCodePromptSubmit() - } else if (e.key === 'Escape') { - e.preventDefault() - setCodePromptInput('') - setIsCodePromptActive(false) - } + if (codeError) setCodeError(null) } const handleDelete = async () => { @@ -805,16 +306,15 @@ try { }) logger.info(`Deleted tool: ${toolId}`) - if (onDelete) { - onDelete(toolId) - } + onDelete?.(toolId) handleClose() } catch (error) { logger.error('Error deleting custom tool:', error) - const errorMessage = getErrorMessage(error, 'Failed to delete custom tool') - setSchemaError(`${errorMessage}. Please try again.`) - setActiveSection('schema') + // A delete failure is not a schema problem — keep it out of the Schema slot. + toast.error("Couldn't delete tool", { + description: getErrorMessage(error, 'Please try again in a moment.'), + }) setShowDeleteConfirm(false) } } @@ -840,307 +340,47 @@ try { the caret as the body scrolls. */} - setActiveSection(value as ToolSection)} - /> +
+
+ setActiveSection(value as ToolSection)} + /> + {activeError && {activeError}} +
+ {/* + Keyed by section so switching tabs resets the inline prompt — one + control drives both wands, and a half-typed Schema prompt must not + carry over and generate Code instead. + */} + activeGeneration.generateStream({ prompt })} + /> +
{activeSection === 'schema' && ( -
-
-
- - {schemaError && ( -
- - {schemaError} -
- )} -
-
- {!isSchemaPromptActive ? ( - - ) : ( -
- handleSchemaPromptChange(e.target.value)} - onBlur={handleSchemaPromptBlur} - onKeyDown={handleSchemaPromptKeyDown} - disabled={schemaGeneration.isStreaming} - className={cn( - 'h-5 max-w-[200px] flex-1 text-xs', - schemaGeneration.isStreaming && 'text-muted-foreground' - )} - placeholder='Generate...' - /> - -
- )} -
-
- -
+ )} {activeSection === 'code' && ( -
-
-
- - {codeError && !codeGeneration.isStreaming && ( -
- - {codeError} -
- )} -
-
- {!isCodePromptActive ? ( - - ) : ( -
- handleCodePromptChange(e.target.value)} - onBlur={handleCodePromptBlur} - onKeyDown={handleCodePromptKeyDown} - disabled={codeGeneration.isStreaming} - className={cn( - 'h-5 max-w-[200px] flex-1 text-xs', - codeGeneration.isStreaming && 'text-muted-foreground' - )} - placeholder='Generate...' - /> - -
- )} -
-
- {schemaParameters.length > 0 && ( -
-
- - Available parameters: - - {schemaParameters.map((param) => ( - - {param.name} - - ))} - - Start typing a parameter name for autocomplete. - -
-
- )} -
- 0 ? '380px' : '420px'} - className={cn( - 'bg-[var(--bg)]', - codeError && !codeGeneration.isStreaming && 'border-[var(--text-error)]', - (codeGeneration.isLoading || codeGeneration.isStreaming) && - 'cursor-not-allowed opacity-50' - )} - gutterClassName='bg-[var(--bg)]' - highlightVariables={true} - disabled={codeGeneration.isLoading || codeGeneration.isStreaming} - onKeyDown={handleKeyDown} - schemaParameters={schemaParameters} - /> - - {showEnvVars && ( - { - setShowEnvVars(false) - setSearchTerm('') - }} - className='w-64' - style={{ - position: 'absolute', - top: `${dropdownPosition.top}px`, - left: `${dropdownPosition.left}px`, - }} - /> - )} - - {showTags && ( - { - setShowTags(false) - setActiveSourceBlockId(null) - }} - className='w-64' - style={{ - position: 'absolute', - top: `${dropdownPosition.top}px`, - left: `${dropdownPosition.left}px`, - }} - /> - )} - - {showSchemaParams && schemaParameters.length > 0 && ( - { - if (!open) { - setShowSchemaParams(false) - } - }} - colorScheme='inverted' - > - -
- - e.preventDefault()} - onCloseAutoFocus={(e) => e.preventDefault()} - > - - Available Parameters - {schemaParameters.map((param, index) => ( - setSchemaParamSelectedIndex(index)} - onMouseDown={(e) => { - e.preventDefault() - e.stopPropagation() - handleSchemaParamSelect(param.name) - }} - ref={(el) => { - if (el) { - schemaParamItemRefs.current.set(index, el) - } - }} - > - {param.name} - {param.type && param.type !== 'any' && ( - - {param.type} - - )} - - ))} - - - - )} -
-
+ )} @@ -1181,7 +421,7 @@ try { primaryAction={{ label: isEditing ? 'Update Tool' : 'Save Tool', onClick: handleSave, - disabled: !isSchemaValid || !!schemaError || !hasChanges, + disabled: !isSchemaValid || !!schemaError || !canSave, }} /> )} @@ -1192,13 +432,7 @@ try { onOpenChange={setShowDeleteConfirm} srTitle='Delete Custom Tool' title='Delete Custom Tool' - text={[ - { - text: 'This will permanently delete the tool and remove it from any workflows that are using it.', - error: true, - }, - ' This action cannot be undone.', - ]} + text={CUSTOM_TOOL_DELETE_CONFIRM_TEXT} confirm={{ label: 'Delete', onClick: handleDelete, diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-wand.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-wand.ts index 1ac9e04257..dc6bd65b33 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-wand.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-wand.ts @@ -136,17 +136,6 @@ export function useWand({ setError(null) }, []) - const openPrompt = useCallback(() => { - setIsPromptVisible(true) - setPromptInputValue('') - }, []) - - const closePrompt = useCallback(() => { - if (isLoading) return - setIsPromptVisible(false) - setPromptInputValue('') - }, [isLoading]) - const generateStream = useCallback( async ({ prompt }: { prompt: string }) => { if (!prompt) { @@ -294,8 +283,6 @@ export function useWand({ generateStream, showPromptInline, hidePromptInline, - openPrompt, - closePrompt, updatePromptValue, cancelGeneration, } diff --git a/apps/sim/components/permissions/add-people-modal.tsx b/apps/sim/components/permissions/add-people-modal.tsx new file mode 100644 index 0000000000..dbfe6d129d --- /dev/null +++ b/apps/sim/components/permissions/add-people-modal.tsx @@ -0,0 +1,171 @@ +'use client' + +import { useCallback, useMemo, useState } from 'react' +import { + ChipModal, + ChipModalBody, + ChipModalError, + ChipModalField, + ChipModalFooter, + ChipModalHeader, +} from '@sim/emcn' +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { partitionSettledFailures, resolveAddEmail } from '@/lib/workspaces/sharing' +import { useWorkspacePermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' +import { MEMBER_ROLE_OPTIONS, type MemberRole } from './member-role-options' + +const logger = createLogger('AddPeopleModal') + +export interface AddPeopleTarget { + email: string + userId: string +} + +interface AddPeopleModalProps { + open: boolean + onOpenChange: (open: boolean) => void + /** Lowercased emails that already have access — rejected as duplicates. */ + existingMemberEmails: Set + /** Grants one person the role; a rejection surfaces as a partial failure. */ + addMember: (target: AddPeopleTarget, role: MemberRole) => Promise + /** + * Hides the Role field for resources without per-member roles (skills): + * every add is a plain grant and `addMember` receives the default role. + */ + hideRole?: boolean +} + +/** + * Shared "Add people" modal for member-managed resources (credentials, skills): + * grants existing workspace members access, optionally with a chosen role. + * Emails are validated against the workspace roster and current membership; + * each add is an idempotent upsert and partial failures keep only the people + * that still need adding. + */ +export function AddPeopleModal({ + open, + onOpenChange, + existingMemberEmails, + addMember, + hideRole = false, +}: AddPeopleModalProps) { + const { workspacePermissions } = useWorkspacePermissionsContext() + + const [emailsToAdd, setEmailsToAdd] = useState([]) + const [roleToAdd, setRoleToAdd] = useState('member') + const [isAdding, setIsAdding] = useState(false) + const [submitError, setSubmitError] = useState(null) + + const workspaceUserIdByEmail = useMemo( + () => + new Map( + (workspacePermissions?.users ?? []).map((user) => [user.email.toLowerCase(), user.userId]) + ), + [workspacePermissions?.users] + ) + + const validateAddEmail = useCallback( + (email: string): string | null => { + const result = resolveAddEmail(email, { workspaceUserIdByEmail, existingMemberEmails }) + return 'error' in result ? result.error : null + }, + [workspaceUserIdByEmail, existingMemberEmails] + ) + + const handleClose = useCallback(() => { + setEmailsToAdd([]) + setRoleToAdd('member') + setSubmitError(null) + onOpenChange(false) + }, [onOpenChange]) + + const handleAddPeople = useCallback(async () => { + if (emailsToAdd.length === 0 || isAdding) return + setSubmitError(null) + const targets = emailsToAdd + .map((email) => { + const result = resolveAddEmail(email, { workspaceUserIdByEmail, existingMemberEmails }) + return 'userId' in result ? { email, userId: result.userId } : null + }) + .filter((target): target is AddPeopleTarget => target !== null) + if (targets.length === 0) return + + setIsAdding(true) + try { + const results = await Promise.allSettled( + targets.map((target) => addMember(target, roleToAdd)) + ) + const failures = partitionSettledFailures(targets, results) + if (failures.length === 0) { + handleClose() + return + } + setEmailsToAdd(failures.map((target) => target.email)) + const firstError = results.find( + (result): result is PromiseRejectedResult => result.status === 'rejected' + ) + logger.error('Failed to add some members', firstError?.reason) + const reason = getErrorMessage(firstError?.reason, 'Please try again in a moment.') + setSubmitError( + failures.length === targets.length + ? `Couldn't add people. ${reason}` + : `Couldn't add ${failures.length} of ${targets.length} people. ${reason}` + ) + } finally { + setIsAdding(false) + } + }, [ + emailsToAdd, + isAdding, + workspaceUserIdByEmail, + existingMemberEmails, + roleToAdd, + addMember, + handleClose, + ]) + + return ( + { + if (!next) handleClose() + }} + srTitle='Add people' + > + Add people + + + {!hideRole && ( + setRoleToAdd(role as MemberRole)} + disabled={isAdding} + /> + )} + {submitError} + + + + ) +} diff --git a/apps/sim/components/permissions/index.ts b/apps/sim/components/permissions/index.ts index 69aff65d94..6fd107d31c 100644 --- a/apps/sim/components/permissions/index.ts +++ b/apps/sim/components/permissions/index.ts @@ -1,3 +1,12 @@ +export { AddPeopleModal, type AddPeopleTarget } from './add-people-modal' +export { + MEMBER_ROLE_OPTIONS, + type MemberRole, + type MemberRoleOption, + SKILL_EDITOR_ROLE_OPTIONS, + type SkillEditorRole, +} from './member-role-options' +export { MemberRow, type MemberRowMember } from './member-row' export { type OrgRole, OrgRoleSelector, @@ -8,6 +17,7 @@ export { type CredentialRoleSource, credentialRoleLockReason, RoleLockTooltip, + skillEditorLockReason, type WorkspaceRoleSource, workspaceRoleLockReason, } from './role-lock' diff --git a/apps/sim/components/permissions/member-role-options.ts b/apps/sim/components/permissions/member-role-options.ts new file mode 100644 index 0000000000..321f91149e --- /dev/null +++ b/apps/sim/components/permissions/member-role-options.ts @@ -0,0 +1,28 @@ +/** Role assignable to a member of a shared resource (credential, skill). */ +export type MemberRole = 'member' | 'admin' + +export interface MemberRoleOption { + value: MemberRole + label: string +} + +/** + * Roles assignable to a resource member. Shared by every member-management + * surface (credential detail, skill members) so role choices never drift. + */ +export const MEMBER_ROLE_OPTIONS: readonly MemberRoleOption[] = [ + { value: 'member', label: 'Member' }, + { value: 'admin', label: 'Admin' }, +] as const + +export type SkillEditorRole = 'editor' + +/** + * Skill membership is binary — a user either edits the skill or does not, and + * `skill_member` has no role column. The single option keeps the roster's role + * control identical in shape to the credential surface's while being honest + * that there is nothing to switch between; every skill row renders it disabled. + */ +export const SKILL_EDITOR_ROLE_OPTIONS: readonly { value: SkillEditorRole; label: string }[] = [ + { value: 'editor', label: 'Editor' }, +] as const diff --git a/apps/sim/components/permissions/member-row.tsx b/apps/sim/components/permissions/member-row.tsx new file mode 100644 index 0000000000..8a23558420 --- /dev/null +++ b/apps/sim/components/permissions/member-row.tsx @@ -0,0 +1,90 @@ +'use client' + +import { Avatar, AvatarFallback, Chip, ChipDropdown, cn } from '@sim/emcn' +import { getUserColor } from '@/lib/workspaces/colors' +import type { MemberRole } from './member-role-options' +import { RoleLockTooltip } from './role-lock' + +export interface MemberRowMember { + userId: string + userName: string | null + userEmail: string | null + role: TRole +} + +interface MemberRowProps { + member: MemberRowMember + /** Why the role is fixed (derived access); null when editable. */ + lockReason: string | null + /** Whether the viewer can act on rows (shows the Remove column). */ + canManage: boolean + roleDisabled: boolean + removeDisabled: boolean + /** + * Role choices for the dropdown. A surface whose membership is binary + * (skills) passes its own single option and always sets `roleDisabled`. + */ + roleOptions: readonly { value: TRole; label: string }[] + /** Omitted on surfaces with nothing to switch between (the role stays disabled). */ + onRoleChange?: (role: TRole) => void + onRemove: () => void +} + +/** + * One member row of a shared-resource member list: avatar + identity, a role + * dropdown (wrapped in a lock tooltip when the role is derived), and a remove + * action for managers. Consumers own the policy (who is locked/disabled); this + * row owns the chrome. + */ +export function MemberRow({ + member, + lockReason, + canManage, + roleDisabled, + removeDisabled, + roleOptions, + onRoleChange, + onRemove, +}: MemberRowProps) { + return ( +
+
+ + + {(member.userName || member.userEmail || '?').charAt(0).toUpperCase()} + + +
+ + {member.userName || member.userEmail || member.userId} + + + {member.userEmail || member.userId} + +
+
+ + onRoleChange?.(role as TRole)} + /> + + {canManage && ( + + Remove + + )} +
+ ) +} diff --git a/apps/sim/components/permissions/role-lock.tsx b/apps/sim/components/permissions/role-lock.tsx index 6988b1200c..db38451693 100644 --- a/apps/sim/components/permissions/role-lock.tsx +++ b/apps/sim/components/permissions/role-lock.tsx @@ -31,6 +31,15 @@ export function credentialRoleLockReason( return null } +/** + * Explanation shown when a skill editor's access is inherited from their + * workspace admin role rather than an explicit grant, and so cannot be removed. + * Returns null for explicitly added editors. + */ +export function skillEditorLockReason(isWorkspaceAdmin: boolean): string | null { + return isWorkspaceAdmin ? 'Workspace admins are automatically skill editors' : null +} + interface RoleLockTooltipProps { reason: string | null children: ReactNode diff --git a/apps/sim/ee/workspace-forking/lib/copy/copy-resources.test.ts b/apps/sim/ee/workspace-forking/lib/copy/copy-resources.test.ts index d3863d3fb6..e7413a0623 100644 --- a/apps/sim/ee/workspace-forking/lib/copy/copy-resources.test.ts +++ b/apps/sim/ee/workspace-forking/lib/copy/copy-resources.test.ts @@ -545,14 +545,26 @@ describe('copyForkResourceContainers external MCP server copy', () => { }) describe('copyForkResourceContainers skill copy', () => { - function makeSkillTx(rows: Array>) { + /** Sequential tx mock: each select resolves the next queued row set (skill rows, then member rows). */ + function makeSkillTx(selects: Array>>) { + let call = 0 const inserted: Array> = [] const tx = { - select: () => ({ from: () => ({ where: () => Promise.resolve(rows) }) }), + select: () => { + const result = Promise.resolve(selects[call++] ?? []) + const chain = { + from: () => chain, + innerJoin: () => chain, + where: () => result, + } + return chain + }, insert: () => ({ values: (values: Array>) => { inserted.push(...values) - return Promise.resolve() + return Object.assign(Promise.resolve(), { + onConflictDoNothing: () => Promise.resolve(), + }) }, }), } @@ -568,20 +580,20 @@ describe('copyForkResourceContainers skill copy', () => { knowledgeBases: [], } + const sourceSkillRow = { + id: 'sk-1', + name: 'My Skill', + description: 'desc', + workspaceId: 'src-ws', + userId: 'src-user', + createdAt: new Date(), + updatedAt: new Date(), + } + it('copies the skill body IN-DB and carries only the child id in the content plan', async () => { // The source projection deliberately omits `content` (it is copied server-side), so the row // fed to the tx mock has none - the body must never be materialized in app memory here. - const { tx, inserted } = makeSkillTx([ - { - id: 'sk-1', - name: 'My Skill', - description: 'desc', - workspaceId: 'src-ws', - userId: 'src-user', - createdAt: new Date(), - updatedAt: new Date(), - }, - ]) + const { tx, inserted } = makeSkillTx([[sourceSkillRow], []]) const result = await copyForkResourceContainers({ tx, @@ -604,6 +616,37 @@ describe('copyForkResourceContainers skill copy', () => { expect(result.contentPlan.skills).toEqual([{ childId }]) expect(result.names.skills).toEqual(['My Skill']) }) + + it('copies editor grants onto the child skill for users in the target roster', async () => { + // The editor query joins the child-workspace permissions in-DB, so the + // mock's second row set already represents source editors ∩ target roster. + const { tx, inserted } = makeSkillTx([ + [sourceSkillRow], + [ + { skillId: 'sk-1', userId: 'editor-1' }, + { skillId: 'sk-1', userId: 'editor-2' }, + ], + ]) + + await copyForkResourceContainers({ + tx, + sourceWorkspaceId: 'src-ws', + childWorkspaceId: 'child-ws', + userId: 'user-1', + now: new Date(), + selection: skillSelection, + workflowIdMap: new Map(), + }) + + const childSkill = inserted[0] + const firstGrant = inserted[1] + expect(firstGrant.skillId).toBe(childSkill.id) + expect(firstGrant.userId).toBe('editor-1') + + const secondGrant = inserted[2] + expect(secondGrant.skillId).toBe(childSkill.id) + expect(secondGrant.userId).toBe('editor-2') + }) }) describe('copyForkResourceContainers knowledge-base tag definitions', () => { diff --git a/apps/sim/ee/workspace-forking/lib/copy/copy-resources.ts b/apps/sim/ee/workspace-forking/lib/copy/copy-resources.ts index 2c7eda693b..63c32d5654 100644 --- a/apps/sim/ee/workspace-forking/lib/copy/copy-resources.ts +++ b/apps/sim/ee/workspace-forking/lib/copy/copy-resources.ts @@ -6,7 +6,9 @@ import { knowledgeBase, knowledgeBaseTagDefinitions, mcpServers, + permissions, skill, + skillMember, userTableDefinitions, userTableRows, workflowMcpServer, @@ -335,6 +337,7 @@ export async function copyForkResourceContainers( .from(skill) .where(and(inArray(skill.id, selection.skills), eq(skill.workspaceId, sourceWorkspaceId))) const inserts: SkillSkeletonInsert[] = [] + const childSkillIdBySource = new Map() for (const row of rows) { const childId = generateId() inserts.push({ @@ -348,11 +351,52 @@ export async function copyForkResourceContainers( createdAt: now, updatedAt: now, }) + childSkillIdBySource.set(row.id, childId) record('skill', row.id, childId) contentPlan.skills.push({ childId }) names.skills.push(row.name) } - if (inserts.length > 0) await tx.insert(skill).values(inserts) + if (inserts.length > 0) { + await tx.insert(skill).values(inserts) + + // Copy editor grants for users who are members of the child workspace. + // Workspace admins need no rows — they are derived editors in the child + // too (mirrors credential member propagation otherwise). + const memberRows = await tx + .select({ + skillId: skillMember.skillId, + userId: skillMember.userId, + }) + .from(skillMember) + .innerJoin( + permissions, + and( + eq(permissions.userId, skillMember.userId), + eq(permissions.entityType, 'workspace'), + eq(permissions.entityId, childWorkspaceId) + ) + ) + .where(inArray(skillMember.skillId, Array.from(childSkillIdBySource.keys()))) + const memberInserts = memberRows.flatMap((member) => { + const childSkillId = childSkillIdBySource.get(member.skillId) + if (!childSkillId) return [] + return [ + { + id: generateId(), + skillId: childSkillId, + userId: member.userId, + createdAt: now, + updatedAt: now, + }, + ] + }) + if (memberInserts.length > 0) { + await tx + .insert(skillMember) + .values(memberInserts) + .onConflictDoNothing({ target: [skillMember.skillId, skillMember.userId] }) + } + } } if (selection.mcpServers.length > 0) { diff --git a/apps/sim/executor/handlers/agent/skills-resolver.test.ts b/apps/sim/executor/handlers/agent/skills-resolver.test.ts index fb9103da2b..bfc68245b2 100644 --- a/apps/sim/executor/handlers/agent/skills-resolver.test.ts +++ b/apps/sim/executor/handlers/agent/skills-resolver.test.ts @@ -3,20 +3,25 @@ */ import { dbChainMockFns, queueTableRows, resetDbChainMock, schemaMock } from '@sim/testing' import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' -import { resolveSkillContent } from './skills-resolver' +import { + resolveSkillContent, + resolveSkillContentById, + resolveSkillMetadata, +} from './skills-resolver' + +// resolveSkillContent is the shared resolver invoked when a workflow agent +// block calls load_skill. Skill editors gate editing only — resolution never +// blocks on the acting user. +beforeEach(() => { + vi.clearAllMocks() + resetDbChainMock() +}) + +afterAll(() => { + resetDbChainMock() +}) -// resolveSkillContent is the shared resolver invoked when a workflow agent block -// calls load_skill. describe('resolveSkillContent', () => { - beforeEach(() => { - vi.clearAllMocks() - resetDbChainMock() - }) - - afterAll(() => { - resetDbChainMock() - }) - it('returns null without a skill name or workspace', async () => { expect(await resolveSkillContent('', 'ws-1')).toBeNull() expect(await resolveSkillContent('x', '')).toBeNull() @@ -29,7 +34,7 @@ describe('resolveSkillContent', () => { }) it('resolves a workspace user skill by name', async () => { - queueTableRows(schemaMock.skill, [{ content: '# Playbook', name: 'posthog-playbook' }]) + queueTableRows(schemaMock.skill, [{ content: '# Playbook' }]) expect(await resolveSkillContent('posthog-playbook', 'ws-1')).toBe('# Playbook') }) @@ -37,3 +42,30 @@ describe('resolveSkillContent', () => { expect(await resolveSkillContent('missing', 'ws-1')).toBeNull() }) }) + +describe('resolveSkillContentById', () => { + it('resolves a workspace skill by id', async () => { + queueTableRows(schemaMock.skill, [{ content: '# Body', name: 'my-skill' }]) + expect(await resolveSkillContentById('sk-1', 'ws-1')).toEqual({ + name: 'my-skill', + content: '# Body', + }) + }) + + it('returns null when the skill does not exist in the workspace', async () => { + expect(await resolveSkillContentById('missing', 'ws-1')).toBeNull() + }) +}) + +describe('resolveSkillMetadata', () => { + it('returns every attached skill in the workspace', async () => { + queueTableRows(schemaMock.skill, [ + { id: 'sk-1', name: 'a', description: 'A' }, + { id: 'sk-2', name: 'b', description: 'B' }, + ]) + + const metadata = await resolveSkillMetadata([{ skillId: 'sk-1' }, { skillId: 'sk-2' }], 'ws-1') + + expect(metadata.map((m) => m.name)).toEqual(['a', 'b']) + }) +}) diff --git a/apps/sim/executor/handlers/agent/skills-resolver.ts b/apps/sim/executor/handlers/agent/skills-resolver.ts index b5a145562e..b14024ec57 100644 --- a/apps/sim/executor/handlers/agent/skills-resolver.ts +++ b/apps/sim/executor/handlers/agent/skills-resolver.ts @@ -45,11 +45,11 @@ export async function resolveSkillMetadata( try { const rows = await db - .select({ name: skill.name, description: skill.description }) + .select({ id: skill.id, name: skill.name, description: skill.description }) .from(skill) .where(and(eq(skill.workspaceId, workspaceId), inArray(skill.id, dbSkillIds))) - return [...metadata, ...rows] + return [...metadata, ...rows.map((row) => ({ name: row.name, description: row.description }))] } catch (error) { logger.error('Failed to resolve skill metadata', { error, dbSkillIds, workspaceId }) return metadata @@ -71,7 +71,7 @@ export async function resolveSkillContent( try { const rows = await db - .select({ content: skill.content, name: skill.name }) + .select({ content: skill.content }) .from(skill) .where(and(eq(skill.workspaceId, workspaceId), eq(skill.name, skillName))) .limit(1) @@ -109,7 +109,7 @@ export async function resolveSkillContentById( return null } - return rows[0] + return { name: rows[0].name, content: rows[0].content } } catch (error) { logger.error('Failed to resolve skill content', { error, skillId, workspaceId }) return null diff --git a/apps/sim/executor/handlers/pi/context.ts b/apps/sim/executor/handlers/pi/context.ts index dfabfd67a5..b6e41e74e5 100644 --- a/apps/sim/executor/handlers/pi/context.ts +++ b/apps/sim/executor/handlers/pi/context.ts @@ -26,7 +26,9 @@ function isMemoryEnabled(config: PiMemoryConfig): boolean { return !!config.memoryType && config.memoryType !== 'none' } -/** Resolves selected skill inputs to full `{ name, content }` entries for Pi. */ +/** + * Resolves selected skill inputs to full `{ name, content }` entries for Pi. + */ export async function resolvePiSkills( skillInputs: unknown, workspaceId: string | undefined @@ -36,15 +38,8 @@ export async function resolvePiSkills( const skills: PiSkill[] = [] for (const input of skillInputs as SkillInput[]) { if (!input?.skillId) continue - try { - const resolved = await resolveSkillContentById(input.skillId, workspaceId) - if (resolved) skills.push({ name: resolved.name, content: resolved.content }) - } catch (error) { - logger.warn('Failed to resolve skill for Pi', { - skillId: input.skillId, - error: getErrorMessage(error), - }) - } + const resolved = await resolveSkillContentById(input.skillId, workspaceId) + if (resolved) skills.push({ name: resolved.name, content: resolved.content }) } return skills } diff --git a/apps/sim/executor/handlers/pi/pi-handler.ts b/apps/sim/executor/handlers/pi/pi-handler.ts index a46637c70b..ebbaa2660d 100644 --- a/apps/sim/executor/handlers/pi/pi-handler.ts +++ b/apps/sim/executor/handlers/pi/pi-handler.ts @@ -135,7 +135,6 @@ export class PiBlockHandler implements BlockHandler { } return this.runPi(ctx, block, runCloudReviewPi, params) } - const memoryConfig: PiMemoryConfig = { memoryType: asOptString(inputs.memoryType) as PiMemoryConfig['memoryType'], conversationId: asOptString(inputs.conversationId), diff --git a/apps/sim/hooks/queries/skills.ts b/apps/sim/hooks/queries/skills.ts index e2bd5a1265..6817f6b624 100644 --- a/apps/sim/hooks/queries/skills.ts +++ b/apps/sim/hooks/queries/skills.ts @@ -3,14 +3,19 @@ import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tansta import { requestJson } from '@/lib/api/client/request' import { deleteSkillContract, + listSkillMembersContract, listSkillsContract, + removeSkillMemberContract, type Skill, + type SkillEditor, + upsertSkillMemberContract, upsertSkillsContract, } from '@/lib/api/contracts' const logger = createLogger('SkillsQueries') export const SKILL_LIST_STALE_TIME = 60 * 1000 +export const SKILL_MEMBER_LIST_STALE_TIME = 30 * 1000 export type SkillDefinition = Skill @@ -21,6 +26,7 @@ export const skillsKeys = { all: ['skills'] as const, lists: () => [...skillsKeys.all, 'list'] as const, list: (workspaceId: string) => [...skillsKeys.lists(), workspaceId] as const, + members: (skillId?: string) => [...skillsKeys.all, 'members', skillId ?? ''] as const, } /** @@ -70,7 +76,13 @@ export function useCreateSkill() { const { data } = await requestJson(upsertSkillsContract, { body: { - skills: [{ name: s.name, description: s.description, content: s.content }], + skills: [ + { + name: s.name, + description: s.description, + content: s.content, + }, + ], workspaceId, }, }) @@ -114,25 +126,11 @@ export function useUpdateSkill() { mutationFn: async ({ workspaceId, skillId, updates }: UpdateSkillParams) => { logger.info(`Updating skill: ${skillId} in workspace ${workspaceId}`) - const currentSkills = queryClient.getQueryData( - skillsKeys.list(workspaceId) - ) - const currentSkill = currentSkills?.find((s) => s.id === skillId) - - if (!currentSkill) { - throw new Error('Skill not found') - } - + // Updates are partial on the wire — omitted fields are preserved + // server-side, so nothing is re-sent from a possibly-stale cache. const { data } = await requestJson(upsertSkillsContract, { body: { - skills: [ - { - id: skillId, - name: updates.name ?? currentSkill.name, - description: updates.description ?? currentSkill.description, - content: updates.content ?? currentSkill.content, - }, - ], + skills: [{ id: skillId, ...updates }], workspaceId, }, }) @@ -172,6 +170,7 @@ export function useUpdateSkill() { }, onSettled: (_data, _error, variables) => { queryClient.invalidateQueries({ queryKey: skillsKeys.list(variables.workspaceId) }) + queryClient.invalidateQueries({ queryKey: skillsKeys.members(variables.skillId) }) }, }) } @@ -224,3 +223,88 @@ export function useDeleteSkill() { }, }) } + +/** + * Fetch the editor roster for a skill (explicit editors plus derived workspace + * admins). Built-in skills have no editors — callers should not enable this + * for readOnly skills. + */ +export function useSkillMembers(skillId?: string, options?: { enabled?: boolean }) { + return useQuery({ + queryKey: skillsKeys.members(skillId), + queryFn: async ({ signal }) => { + if (!skillId) return [] + const data = await requestJson(listSkillMembersContract, { + params: { id: skillId }, + signal, + }) + return data.editors + }, + enabled: Boolean(skillId) && (options?.enabled ?? true), + staleTime: SKILL_MEMBER_LIST_STALE_TIME, + }) +} + +interface UpsertSkillMemberParams { + skillId: string + workspaceId: string + userId: string +} + +export function useUpsertSkillMember() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async ({ skillId, userId }: UpsertSkillMemberParams) => { + return requestJson(upsertSkillMemberContract, { + params: { id: skillId }, + body: { userId }, + }) + }, + onSuccess: (_data, variables) => { + queryClient.invalidateQueries({ queryKey: skillsKeys.members(variables.skillId) }) + queryClient.invalidateQueries({ queryKey: skillsKeys.list(variables.workspaceId) }) + }, + }) +} + +interface RemoveSkillMemberParams { + skillId: string + workspaceId: string + userId: string +} + +export function useRemoveSkillMember() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async ({ skillId, userId }: RemoveSkillMemberParams) => { + return requestJson(removeSkillMemberContract, { + params: { id: skillId }, + query: { userId }, + }) + }, + onMutate: async (variables) => { + await queryClient.cancelQueries({ queryKey: skillsKeys.members(variables.skillId) }) + const previousEditors = queryClient.getQueryData( + skillsKeys.members(variables.skillId) + ) + if (previousEditors) { + queryClient.setQueryData( + skillsKeys.members(variables.skillId), + previousEditors.filter((editor) => editor.userId !== variables.userId) + ) + } + return { previousEditors } + }, + onError: (_err, variables, context) => { + if (context?.previousEditors) { + queryClient.setQueryData(skillsKeys.members(variables.skillId), context.previousEditors) + } + }, + onSettled: (_data, _error, variables) => { + queryClient.invalidateQueries({ queryKey: skillsKeys.members(variables.skillId) }) + queryClient.invalidateQueries({ queryKey: skillsKeys.list(variables.workspaceId) }) + }, + }) +} diff --git a/apps/sim/lib/api/contracts/skills.ts b/apps/sim/lib/api/contracts/skills.ts index 950acf782e..31af51cd48 100644 --- a/apps/sim/lib/api/contracts/skills.ts +++ b/apps/sim/lib/api/contracts/skills.ts @@ -8,6 +8,11 @@ export const skillSchema = z.object({ name: z.string(), description: z.string(), content: z.string(), + /** + * Whether the caller can edit, delete, and share the skill (explicit editor + * or derived workspace admin). Always false for built-in template skills. + */ + canEdit: z.boolean(), createdAt: z.string(), updatedAt: z.string(), /** True for built-in template skills, which are read-only and not stored in the DB. */ @@ -16,17 +21,57 @@ export const skillSchema = z.object({ export type Skill = z.output -export const skillUpsertItemSchema = z.object({ - id: z.string().optional(), - name: z - .string() - .min(1, 'Skill name is required') - .max(64) - .regex(/^[a-z0-9]+(-[a-z0-9]+)*$/, 'Name must be kebab-case (e.g. my-skill)'), - description: z.string().min(1, 'Description is required').max(1024), - content: z.string().min(1, 'Content is required').max(50_000, 'Content is too large'), +/** + * One entry of a skill's editor roster: a workspace admin (derived, always an + * editor) or an explicitly added editor. + */ +export const skillEditorSchema = z.object({ + id: z.string(), + userId: z.string(), + userName: z.string().nullable(), + userEmail: z.string().nullable(), + userImage: z.string().nullable().optional(), + isWorkspaceAdmin: z.boolean(), }) +export type SkillEditor = z.output + +const skillNameSchema = z + .string() + .min(1, 'Skill name is required') + .max(64) + .regex(/^[a-z0-9]+(-[a-z0-9]+)*$/, 'Name must be kebab-case (e.g. my-skill)') +const skillDescriptionSchema = z.string().min(1, 'Description is required').max(1024) +const skillContentSchema = z + .string() + .min(1, 'Content is required') + .max(50_000, 'Content is too large') + +/** + * One skill in an upsert. Creates (no `id`) require name/description/content; + * updates (`id` set) are partial — omitted fields keep their current values + * server-side, so a partial edit can never clobber a concurrent content edit. + */ +export const skillUpsertItemSchema = z + .object({ + id: z.string().min(1).optional(), + name: skillNameSchema.optional(), + description: skillDescriptionSchema.optional(), + content: skillContentSchema.optional(), + }) + .superRefine((item, ctx) => { + if (item.id) return + if (item.name === undefined) { + ctx.addIssue({ code: 'custom', path: ['name'], message: 'Skill name is required' }) + } + if (item.description === undefined) { + ctx.addIssue({ code: 'custom', path: ['description'], message: 'Description is required' }) + } + if (item.content === undefined) { + ctx.addIssue({ code: 'custom', path: ['content'], message: 'Content is required' }) + } + }) + export const listSkillsQuerySchema = z.object({ workspaceId: z.string().min(1), }) @@ -43,10 +88,6 @@ export const deleteSkillQuerySchema = z.object({ source: z.enum(['settings', 'tool_input']).optional(), }) -export const importSkillBodySchema = z.object({ - url: z.string().url('A valid URL is required'), -}) - export const listSkillsContract = defineRouteContract({ method: 'GET', path: '/api/skills', @@ -84,14 +125,54 @@ export const deleteSkillContract = defineRouteContract({ }, }) -export const importSkillContract = defineRouteContract({ - method: 'POST', - path: '/api/skills/import', - body: importSkillBodySchema, +export const skillIdParamsSchema = z.object({ + id: z.string().min(1, 'Skill id is required'), +}) + +export const upsertSkillMemberBodySchema = z.object({ + userId: z.string().min(1, 'User id is required'), +}) + +export type UpsertSkillMemberBody = z.input + +export const removeSkillMemberQuerySchema = z.object({ + userId: z.string().min(1, 'User id is required'), +}) + +export const listSkillMembersContract = defineRouteContract({ + method: 'GET', + path: '/api/skills/[id]/members', + params: skillIdParamsSchema, response: { mode: 'json', schema: z.object({ - content: z.string(), + editors: z.array(skillEditorSchema), + }), + }, +}) + +export const upsertSkillMemberContract = defineRouteContract({ + method: 'POST', + path: '/api/skills/[id]/members', + params: skillIdParamsSchema, + body: upsertSkillMemberBodySchema, + response: { + mode: 'json', + schema: z.object({ + success: z.literal(true), + }), + }, +}) + +export const removeSkillMemberContract = defineRouteContract({ + method: 'DELETE', + path: '/api/skills/[id]/members', + params: skillIdParamsSchema, + query: removeSkillMemberQuerySchema, + response: { + mode: 'json', + schema: z.object({ + success: z.literal(true), }), }, }) diff --git a/apps/sim/lib/billing/organizations/membership.ts b/apps/sim/lib/billing/organizations/membership.ts index 7d2955ef58..3cb64bedef 100644 --- a/apps/sim/lib/billing/organizations/membership.ts +++ b/apps/sim/lib/billing/organizations/membership.ts @@ -47,6 +47,7 @@ import { enqueueOutboxEvent } from '@/lib/core/outbox/service' import { revokeWorkspaceCredentialMembershipsTx } from '@/lib/credentials/access' import type { DbOrTx } from '@/lib/db/types' import { acquireInvitationMutationLocks } from '@/lib/invitations/locks' +import { removeWorkspaceSkillMembershipsTx } from '@/lib/skills/access' import { reassignWorkflowOwnershipForWorkspaceMemberRemovalTx, WorkspaceBillingAccountRemovalError, @@ -1470,6 +1471,7 @@ export async function transferUserBetweenOrganizations( workspaceIds, params.userId ) + await removeWorkspaceSkillMembershipsTx(tx, workspaceIds, params.userId) } const [stats] = await tx @@ -1721,6 +1723,7 @@ export async function removeUserFromOrganization( workspaceIds, userId ) + await removeWorkspaceSkillMembershipsTx(tx, workspaceIds, userId) const capturedUsage = await captureDepartedUsage() return { @@ -1935,6 +1938,7 @@ export async function removeExternalUserFromOrganizationWorkspaces(params: { workspaceIds, userId ) + await removeWorkspaceSkillMembershipsTx(tx, workspaceIds, userId) return { workspaceAccessRevoked: deletedPermissions.length, diff --git a/apps/sim/lib/copilot/chat/workspace-context.ts b/apps/sim/lib/copilot/chat/workspace-context.ts index 11ee6ac370..5276e3257f 100644 --- a/apps/sim/lib/copilot/chat/workspace-context.ts +++ b/apps/sim/lib/copilot/chat/workspace-context.ts @@ -26,11 +26,11 @@ import { import { listWorkspaceFiles } from '@/lib/uploads/contexts/workspace' import { listCustomBlockSummariesForWorkspace } from '@/lib/workflows/custom-blocks/operations' import { listCustomTools } from '@/lib/workflows/custom-tools/operations' -import { listSkills } from '@/lib/workflows/skills/operations' +import { listSkillsForUser } from '@/lib/workflows/skills/operations' import { assertActiveWorkspaceAccess, getUsersWithPermissions, - getWorkspaceWithOwner, + type WorkspaceAccess, } from '@/lib/workspaces/permissions/utils' const logger = createLogger('WorkspaceContext') @@ -334,11 +334,17 @@ export function buildWorkspaceContextMd(data: WorkspaceMdData): string { // workspace is unavailable or a fetch fails. async function buildWorkspaceMdData( workspaceId: string, - userId: string + userId: string, + options?: { workspaceAccess?: WorkspaceAccess } ): Promise { try { - await assertActiveWorkspaceAccess(workspaceId, userId) - const wsRow = await getWorkspaceWithOwner(workspaceId) + // Reuse the caller's already-asserted access when provided (hot chat path); + // the id match keeps a mismatched cache from authorizing this workspace. + const workspaceAccess = + options?.workspaceAccess && options.workspaceAccess.workspace?.id === workspaceId + ? options.workspaceAccess + : await assertActiveWorkspaceAccess(workspaceId, userId) + const wsRow = workspaceAccess.hasAccess ? workspaceAccess.workspace : null if (!wsRow) { return null } @@ -421,7 +427,7 @@ async function buildWorkspaceMdData( .from(mcpServers) .where(and(eq(mcpServers.workspaceId, workspaceId), isNull(mcpServers.deletedAt))), - listSkills({ workspaceId, includeBuiltins: false }), + listSkillsForUser({ workspaceId, userId, includeBuiltins: false, workspaceAccess }), db .select({ @@ -555,9 +561,10 @@ const WORKSPACE_CONTEXT_UNAVAILABLE_MD = */ export async function generateWorkspaceContext( workspaceId: string, - userId: string + userId: string, + options?: { workspaceAccess?: WorkspaceAccess } ): Promise { - const data = await buildWorkspaceMdData(workspaceId, userId) + const data = await buildWorkspaceMdData(workspaceId, userId, options) return data ? buildWorkspaceMd(data) : WORKSPACE_CONTEXT_UNAVAILABLE_MD } diff --git a/apps/sim/lib/copilot/tools/handlers/management/manage-skill.ts b/apps/sim/lib/copilot/tools/handlers/management/manage-skill.ts index dc551e382e..e3c1c7bfeb 100644 --- a/apps/sim/lib/copilot/tools/handlers/management/manage-skill.ts +++ b/apps/sim/lib/copilot/tools/handlers/management/manage-skill.ts @@ -3,7 +3,9 @@ import { createLogger } from '@sim/logger' import { getErrorMessage, toError } from '@sim/utils/errors' import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/request/types' import { captureServerEvent } from '@/lib/posthog/server' -import { deleteSkill, listSkills, upsertSkills } from '@/lib/workflows/skills/operations' +import { getSkillActorContext } from '@/lib/skills/access' +import { isBuiltinSkillId } from '@/lib/workflows/skills/builtin-skills' +import { deleteSkill, listSkillsForUser, upsertSkills } from '@/lib/workflows/skills/operations' const logger = createLogger('CopilotToolExecutor') @@ -33,22 +35,23 @@ export async function executeManageSkill( return { success: false, error: 'workspaceId is required' } } - const writeOps: string[] = ['add', 'edit', 'delete'] + // Workspace write gates only creation; edits and deletes are gated per skill + // below (skill editor — explicit editor row or derived workspace admin). if ( - writeOps.includes(operation) && + operation === 'add' && context.userPermission && context.userPermission !== 'write' && context.userPermission !== 'admin' ) { return { success: false, - error: `Permission denied: '${operation}' on manage_skill requires write access. You have '${context.userPermission}' permission.`, + error: `Permission denied: 'add' on manage_skill requires write access. You have '${context.userPermission}' permission.`, } } try { if (operation === 'list') { - const skills = await listSkills({ workspaceId }) + const skills = await listSkillsForUser({ workspaceId, userId: context.userId }) return { success: true, @@ -128,26 +131,36 @@ export async function executeManageSkill( } } - const existing = await listSkills({ workspaceId }) - const found = existing.find((s) => s.id === params.skillId) - if (!found) { - return { success: false, error: `Skill not found: ${params.skillId}` } + if (isBuiltinSkillId(params.skillId)) { + return { success: false, error: 'Built-in skills are read-only and cannot be modified' } } + const actor = await getSkillActorContext(params.skillId, context.userId) + if (!actor.skill || actor.skill.workspaceId !== workspaceId || !actor.hasWorkspaceAccess) { + return { success: false, error: `Skill not found: ${params.skillId}` } + } + if (!actor.canEdit) { + return { + success: false, + error: `Permission denied: editing skill "${actor.skill.name}" requires skill editor access. Ask a skill editor to add you.`, + } + } + + // Partial update: omitted fields keep their current values server-side. await upsertSkills({ skills: [ { id: params.skillId, - name: params.name || found.name, - description: params.description || found.description, - content: params.content || found.content, + ...(params.name ? { name: params.name } : {}), + ...(params.description ? { description: params.description } : {}), + ...(params.content ? { content: params.content } : {}), }, ], workspaceId, userId: context.userId, }) - const updatedName = params.name || found.name + const updatedName = params.name || actor.skill.name recordAudit({ workspaceId, actorId: context.userId, @@ -176,8 +189,8 @@ export async function executeManageSkill( success: true, operation, skillId: params.skillId, - name: params.name || found.name, - message: `Updated skill "${params.name || found.name}"`, + name: updatedName, + message: `Updated skill "${updatedName}"`, }, } } @@ -187,6 +200,19 @@ export async function executeManageSkill( return { success: false, error: "'skillId' is required for 'delete'" } } + if (!isBuiltinSkillId(params.skillId)) { + const actor = await getSkillActorContext(params.skillId, context.userId) + if (!actor.skill || actor.skill.workspaceId !== workspaceId || !actor.hasWorkspaceAccess) { + return { success: false, error: `Skill not found: ${params.skillId}` } + } + if (!actor.canEdit) { + return { + success: false, + error: `Permission denied: deleting skill "${actor.skill.name}" requires skill editor access. Ask a skill editor to add you.`, + } + } + } + const deleted = await deleteSkill({ skillId: params.skillId, workspaceId }) if (!deleted) { return { success: false, error: `Skill not found: ${params.skillId}` } diff --git a/apps/sim/lib/copilot/vfs/workspace-vfs.ts b/apps/sim/lib/copilot/vfs/workspace-vfs.ts index cf3d48816e..eeb6516f9f 100644 --- a/apps/sim/lib/copilot/vfs/workspace-vfs.ts +++ b/apps/sim/lib/copilot/vfs/workspace-vfs.ts @@ -2154,9 +2154,11 @@ export class WorkspaceVFS { } /** - * Advertise workspace skills in the VFS without eagerly loading their bodies. - * Paths are registered as lazy so glob/WORKSPACE.md see them, but full content - * is fetched only when read (or a grep whose scope touches the path) resolves them. + * Advertise the workspace skills in the VFS without eagerly loading their + * bodies. Paths are registered as lazy so glob/WORKSPACE.md see them, but + * full content is fetched only when read (or a grep whose scope touches the + * path) resolves them. Skills are workspace-visible — everyone with + * workspace access sees and uses every skill. */ private async materializeSkills( workspaceId: string diff --git a/apps/sim/lib/credentials/access.test.ts b/apps/sim/lib/credentials/access.test.ts index 0fc2c8dc2f..b5ae09c141 100644 --- a/apps/sim/lib/credentials/access.test.ts +++ b/apps/sim/lib/credentials/access.test.ts @@ -8,6 +8,11 @@ const { mockCheckWorkspaceAccess } = vi.hoisted(() => ({ vi.mock('@/lib/workspaces/permissions/utils', () => ({ checkWorkspaceAccess: mockCheckWorkspaceAccess, + resolveWorkspaceAccess: vi.fn(async (workspaceId: string, userId: string, provided?: any) => + provided && provided.workspace?.id === workspaceId + ? provided + : mockCheckWorkspaceAccess(workspaceId, userId) + ), })) import { getCredentialActorContext } from '@/lib/credentials/access' diff --git a/apps/sim/lib/credentials/access.ts b/apps/sim/lib/credentials/access.ts index 1fd5728982..005a47fe65 100644 --- a/apps/sim/lib/credentials/access.ts +++ b/apps/sim/lib/credentials/access.ts @@ -2,7 +2,7 @@ import { db } from '@sim/db' import { credential, credentialMember, credentialTypeEnum } from '@sim/db/schema' import { and, eq, inArray } from 'drizzle-orm' import type { DbOrTx } from '@/lib/db/types' -import { checkWorkspaceAccess, type WorkspaceAccess } from '@/lib/workspaces/permissions/utils' +import { resolveWorkspaceAccess, type WorkspaceAccess } from '@/lib/workspaces/permissions/utils' type ActiveCredentialMember = typeof credentialMember.$inferSelect type CredentialRecord = typeof credential.$inferSelect @@ -74,11 +74,11 @@ export async function getCredentialActorContext( } } - const providedAccess = options?.workspaceAccess - const workspaceAccess = - providedAccess && providedAccess.workspace?.id === credentialRow.workspaceId - ? providedAccess - : await checkWorkspaceAccess(credentialRow.workspaceId, userId) + const workspaceAccess = await resolveWorkspaceAccess( + credentialRow.workspaceId, + userId, + options?.workspaceAccess + ) const [memberRow] = await db .select() .from(credentialMember) @@ -111,6 +111,13 @@ export async function getCredentialActorContext( * Workspace owners and admins are derived credential admins, so no per-credential * owner promotion is needed to avoid orphaning a credential. Returns the number * of memberships revoked. + * + * Deliberately DIVERGES from the skill sibling (`removeWorkspaceSkillMembershipsTx` + * deletes active rows): credential access is explicit-rows-only, so a revoked + * row here is an inert tombstone the env-credential join sync uses to avoid + * resurrecting access. Skills have an implicit workspace-shared grant, where a + * revoked row is a live per-skill DENY marker and a kept-active row would + * re-grant on rejoin — do not "harmonize" either helper toward the other. */ export async function revokeWorkspaceCredentialMembershipsTx( tx: DbOrTx, diff --git a/apps/sim/lib/mothership/inbox/executor.ts b/apps/sim/lib/mothership/inbox/executor.ts index b04b5655fe..21fcc48c28 100644 --- a/apps/sim/lib/mothership/inbox/executor.ts +++ b/apps/sim/lib/mothership/inbox/executor.ts @@ -25,7 +25,7 @@ import { sendInboxResponse } from '@/lib/mothership/inbox/response' import type { AgentMailAttachment } from '@/lib/mothership/inbox/types' import { uploadFile } from '@/lib/uploads/core/storage-service' import { createFileContent, type MessageContent } from '@/lib/uploads/utils/file-utils' -import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' +import { checkWorkspaceAccess, getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' import { getWorkspaceBilledAccountUserId } from '@/lib/workspaces/utils' const logger = createLogger('InboxExecutor') @@ -210,21 +210,16 @@ export async function executeInboxTask(taskId: string): Promise { return { attachments, ...downloaded } } - const [ - attachmentResult, - workspaceContext, - integrationTools, - userPermission, - billingAttribution, - entitlements, - ] = await Promise.all([ - fetchAttachments(), - generateWorkspaceContext(ws.id, userId), - buildIntegrationToolSchemas(userId, undefined, undefined, ws.id), - getUserEntityPermissions(userId, 'workspace', ws.id).catch(() => null), - resolveBillingAttribution({ actorUserId: userId, workspaceId: ws.id }), - computeWorkspaceEntitlements(ws.id, userId), - ]) + const workspaceAccess = await checkWorkspaceAccess(ws.id, userId) + const userPermission = workspaceAccess.permission + const [attachmentResult, workspaceContext, integrationTools, billingAttribution, entitlements] = + await Promise.all([ + fetchAttachments(), + generateWorkspaceContext(ws.id, userId, { workspaceAccess }), + buildIntegrationToolSchemas(userId, undefined, undefined, ws.id), + resolveBillingAttribution({ actorUserId: userId, workspaceId: ws.id }), + computeWorkspaceEntitlements(ws.id, userId), + ]) const { attachments, fileAttachments, storedAttachments } = attachmentResult const truncatedTask = { diff --git a/apps/sim/lib/posthog/events.ts b/apps/sim/lib/posthog/events.ts index 61534aeb1a..723717d1d3 100644 --- a/apps/sim/lib/posthog/events.ts +++ b/apps/sim/lib/posthog/events.ts @@ -213,6 +213,16 @@ export interface PostHogEventMap { source?: 'settings' | 'tool_input' } + skill_shared: { + skill_id: string + workspace_id: string + } + + skill_unshared: { + skill_id: string + workspace_id: string + } + workspace_deleted: { workspace_id: string workflow_count: number diff --git a/apps/sim/lib/skills/access.test.ts b/apps/sim/lib/skills/access.test.ts new file mode 100644 index 0000000000..d993edec97 --- /dev/null +++ b/apps/sim/lib/skills/access.test.ts @@ -0,0 +1,282 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockCheckWorkspaceAccess, mockGetUsersWithPermissions, dbState, makeChain, dbMock } = + vi.hoisted(() => { + const state = { results: [] as unknown[][] } + const chainFactory = () => { + const resolve = () => Promise.resolve(state.results.shift() ?? []) + const chain: any = {} + chain.from = vi.fn(() => chain) + chain.innerJoin = vi.fn(() => chain) + chain.where = vi.fn(() => chain) + chain.set = vi.fn(() => chain) + chain.limit = vi.fn(() => resolve()) + chain.returning = vi.fn(() => resolve()) + chain.then = (onFulfilled: any, onRejected: any) => resolve().then(onFulfilled, onRejected) + return chain + } + return { + mockCheckWorkspaceAccess: vi.fn(), + mockGetUsersWithPermissions: vi.fn(), + dbState: state, + makeChain: chainFactory, + dbMock: { + select: vi.fn(() => chainFactory()), + update: vi.fn(() => chainFactory()), + }, + } + }) + +vi.mock('@sim/db', () => ({ + db: dbMock, +})) + +vi.mock('@sim/db/schema', () => ({ + skill: { + id: 'skill.id', + workspaceId: 'skill.workspaceId', + name: 'skill.name', + }, + skillMember: { + id: 'skillMember.id', + skillId: 'skillMember.skillId', + userId: 'skillMember.userId', + }, +})) + +vi.mock('drizzle-orm', () => ({ + and: vi.fn((...args: unknown[]) => ({ and: args })), + eq: vi.fn((a: unknown, b: unknown) => ({ eq: [a, b] })), + inArray: vi.fn((a: unknown, b: unknown) => ({ inArray: [a, b] })), +})) + +vi.mock('@/lib/workspaces/permissions/utils', () => ({ + checkWorkspaceAccess: mockCheckWorkspaceAccess, + getUsersWithPermissions: mockGetUsersWithPermissions, + resolveWorkspaceAccess: vi.fn(async (workspaceId: string, userId: string, provided?: any) => + provided && provided.workspace?.id === workspaceId + ? provided + : mockCheckWorkspaceAccess(workspaceId, userId) + ), +})) + +import { + checkSkillsUpdateAccess, + getEditableSkillIds, + getSkillActorContext, + listSkillEditors, + removeWorkspaceSkillMembershipsTx, +} from '@/lib/skills/access' + +const wsAdmin = { hasAccess: true, canWrite: true, canAdmin: true, workspace: { id: 'ws' } } +const wsWrite = { hasAccess: true, canWrite: true, canAdmin: false, workspace: { id: 'ws' } } +const wsRead = { hasAccess: true, canWrite: false, canAdmin: false, workspace: { id: 'ws' } } +const wsNone = { hasAccess: false, canWrite: false, canAdmin: false, workspace: null } + +beforeEach(() => { + vi.clearAllMocks() + dbState.results = [] + mockGetUsersWithPermissions.mockResolvedValue([]) +}) + +describe('getSkillActorContext', () => { + it('grants edit access from an explicit editor row', async () => { + dbState.results = [[{ id: 's1', workspaceId: 'ws' }], [{ id: 'row-1' }]] + mockCheckWorkspaceAccess.mockResolvedValue(wsRead) + + const ctx = await getSkillActorContext('s1', 'user1') + + expect(ctx.hasWorkspaceAccess).toBe(true) + expect(ctx.canEdit).toBe(true) + }) + + it('derives edit access from workspace admin without any rows', async () => { + dbState.results = [[{ id: 's1', workspaceId: 'ws' }], []] + mockCheckWorkspaceAccess.mockResolvedValue(wsAdmin) + + const ctx = await getSkillActorContext('s1', 'admin-user') + + expect(ctx.canEdit).toBe(true) + }) + + it('lets rowless workspace members see but not edit', async () => { + dbState.results = [[{ id: 's1', workspaceId: 'ws' }], []] + mockCheckWorkspaceAccess.mockResolvedValue(wsWrite) + + const ctx = await getSkillActorContext('s1', 'writer') + + expect(ctx.hasWorkspaceAccess).toBe(true) + expect(ctx.canEdit).toBe(false) + }) + + it('denies everything without workspace access, even with an editor row', async () => { + dbState.results = [[{ id: 's1', workspaceId: 'ws' }], [{ id: 'row-1' }]] + mockCheckWorkspaceAccess.mockResolvedValue(wsNone) + + const ctx = await getSkillActorContext('s1', 'outsider') + + expect(ctx.hasWorkspaceAccess).toBe(false) + expect(ctx.canEdit).toBe(false) + }) + + it('returns an empty context when the skill does not exist', async () => { + dbState.results = [[]] + + const ctx = await getSkillActorContext('missing', 'user1') + + expect(ctx.skill).toBeNull() + expect(ctx.hasWorkspaceAccess).toBe(false) + expect(ctx.canEdit).toBe(false) + expect(mockCheckWorkspaceAccess).not.toHaveBeenCalled() + }) +}) + +describe('getEditableSkillIds', () => { + it('collects the editor rows from one workspace-scoped scan', async () => { + dbState.results = [[{ skillId: 's-mine' }, { skillId: 's-also-mine' }]] + mockCheckWorkspaceAccess.mockResolvedValue(wsRead) + + const access = await getEditableSkillIds('ws', 'user1') + + expect(access.canAdminWorkspace).toBe(false) + expect(access.editorSkillIds).toEqual(new Set(['s-mine', 's-also-mine'])) + }) + + it('flags workspace admins as editors of everything', async () => { + dbState.results = [[]] + mockCheckWorkspaceAccess.mockResolvedValue(wsAdmin) + + const access = await getEditableSkillIds('ws', 'admin-user') + + expect(access.canAdminWorkspace).toBe(true) + }) + + it('grants nothing without workspace access, even with editor rows', async () => { + dbState.results = [[{ skillId: 's-mine' }]] + mockCheckWorkspaceAccess.mockResolvedValue(wsNone) + + const access = await getEditableSkillIds('ws', 'outsider') + + expect(access.canAdminWorkspace).toBe(false) + expect(access.editorSkillIds.size).toBe(0) + }) +}) + +describe('listSkillEditors', () => { + const roster = (users: Array<{ userId: string; permissionType: string }>) => + users.map((u) => ({ + userId: u.userId, + permissionType: u.permissionType, + name: `${u.userId}-name`, + email: `${u.userId}@x.com`, + image: null, + })) + + it('lists derived workspace admins plus explicit editors still in the roster', async () => { + dbState.results = [ + [ + { id: 'row-1', userId: 'writer' }, + { id: 'row-2', userId: 'ghost' }, + ], + ] + mockGetUsersWithPermissions.mockResolvedValue( + roster([ + { userId: 'boss', permissionType: 'admin' }, + { userId: 'writer', permissionType: 'write' }, + { userId: 'reader', permissionType: 'read' }, + ]) + ) + + const editors = await listSkillEditors({ id: 's1', workspaceId: 'ws' }) + const byUser = new Map(editors.map((e) => [e.userId, e])) + + expect(byUser.get('boss')).toMatchObject({ + id: 'workspace-admin-boss', + isWorkspaceAdmin: true, + userEmail: 'boss@x.com', + }) + expect(byUser.get('writer')).toMatchObject({ id: 'row-1', isWorkspaceAdmin: false }) + // Workspace members without a row are not editors. + expect(byUser.has('reader')).toBe(false) + // Rows for users no longer in the workspace never render. + expect(byUser.has('ghost')).toBe(false) + }) + + it('keeps a workspace admin flagged as derived even when they hold an explicit row', async () => { + dbState.results = [[{ id: 'row-1', userId: 'boss' }]] + mockGetUsersWithPermissions.mockResolvedValue( + roster([{ userId: 'boss', permissionType: 'admin' }]) + ) + + const editors = await listSkillEditors({ id: 's1', workspaceId: 'ws' }) + + expect(editors).toHaveLength(1) + expect(editors[0]).toMatchObject({ id: 'row-1', userId: 'boss', isWorkspaceAdmin: true }) + }) +}) + +describe('checkSkillsUpdateAccess', () => { + it('returns nothing for an empty id list without querying', async () => { + const result = await checkSkillsUpdateAccess({ workspaceId: 'ws', userId: 'u', skillIds: [] }) + expect(result.existingIds.size).toBe(0) + expect(result.denied).toEqual([]) + expect(dbMock.select).not.toHaveBeenCalled() + }) + + it('partitions resolvable ids and denies skills without an editor row', async () => { + dbState.results = [ + [ + { id: 's-mine', name: 'mine' }, + { id: 's-other', name: 'other' }, + ], + [{ skillId: 's-mine' }], + ] + mockCheckWorkspaceAccess.mockResolvedValue(wsWrite) + + const result = await checkSkillsUpdateAccess({ + workspaceId: 'ws', + userId: 'u', + skillIds: ['s-mine', 's-other', 's-create'], + }) + + expect(result.existingIds).toEqual(new Set(['s-mine', 's-other'])) + expect(result.denied).toEqual([{ id: 's-other', name: 'other' }]) + }) + + it('denies nothing for workspace admins', async () => { + dbState.results = [[{ id: 's-any', name: 'any' }], []] + mockCheckWorkspaceAccess.mockResolvedValue(wsAdmin) + + const result = await checkSkillsUpdateAccess({ + workspaceId: 'ws', + userId: 'admin-user', + skillIds: ['s-any'], + }) + + expect(result.denied).toEqual([]) + }) +}) + +describe('removeWorkspaceSkillMembershipsTx', () => { + it('returns 0 for an empty workspace list without querying', async () => { + const tx = { select: vi.fn(() => makeChain()), delete: vi.fn(() => makeChain()) } + expect(await removeWorkspaceSkillMembershipsTx(tx as never, [], 'u')).toBe(0) + expect(tx.delete).not.toHaveBeenCalled() + }) + + it('deletes every editor grant for the user in the workspaces and counts them', async () => { + const { eq } = await import('drizzle-orm') + dbState.results = [[{ id: 'm1' }, { id: 'm2' }]] + const tx = { select: vi.fn(() => makeChain()), delete: vi.fn(() => makeChain()) } + + expect(await removeWorkspaceSkillMembershipsTx(tx as never, ['ws'], 'u')).toBe(2) + expect(tx.delete).toHaveBeenCalledTimes(1) + // Rows are plain editor grants — the delete has no status filter, so a + // re-invited user starts with no edit rights until re-added. + expect(vi.mocked(eq).mock.calls).toContainEqual(['skillMember.userId', 'u']) + expect(vi.mocked(eq).mock.calls.some(([field]) => field === 'skillMember.status')).toBe(false) + }) +}) diff --git a/apps/sim/lib/skills/access.ts b/apps/sim/lib/skills/access.ts new file mode 100644 index 0000000000..ca835a4741 --- /dev/null +++ b/apps/sim/lib/skills/access.ts @@ -0,0 +1,212 @@ +import { db } from '@sim/db' +import { skill, skillMember } from '@sim/db/schema' +import { and, eq, inArray } from 'drizzle-orm' +import type { DbOrTx } from '@/lib/db/types' +import { + getUsersWithPermissions, + resolveWorkspaceAccess, + type WorkspaceAccess, +} from '@/lib/workspaces/permissions/utils' + +type SkillRecord = typeof skill.$inferSelect + +export interface SkillActorContext { + skill: SkillRecord | null + /** Whether the actor can see and use the skill — plain workspace access. */ + hasWorkspaceAccess: boolean + /** + * Whether the actor can edit, delete, and share the skill: an explicit + * `skill_member` editor row, or derived workspace admin (always, undemotable). + */ + canEdit: boolean +} + +/** + * Resolves the acting user's context for a single skill. Everyone with + * workspace access sees and uses every skill; editing is gated by the editors + * list. Builtin skills are code-only and have no editors; callers guard with + * `isBuiltinSkillId` before reaching this. + */ +export async function getSkillActorContext( + skillId: string, + userId: string +): Promise { + const [skillRow] = await db.select().from(skill).where(eq(skill.id, skillId)).limit(1) + + if (!skillRow?.workspaceId) { + return { skill: skillRow ?? null, hasWorkspaceAccess: false, canEdit: false } + } + + const [workspaceAccess, [editorRow]] = await Promise.all([ + resolveWorkspaceAccess(skillRow.workspaceId, userId), + db + .select({ id: skillMember.id }) + .from(skillMember) + .where(and(eq(skillMember.skillId, skillId), eq(skillMember.userId, userId))) + .limit(1), + ]) + + return { + skill: skillRow, + hasWorkspaceAccess: workspaceAccess.hasAccess, + canEdit: workspaceAccess.hasAccess && (workspaceAccess.canAdmin || !!editorRow), + } +} + +export interface EditableSkillIds { + /** Workspace admins are derived editors of every skill in the workspace. */ + canAdminWorkspace: boolean + /** Skills where the user holds an explicit editor row. */ + editorSkillIds: Set +} + +/** + * Batch edit-access surface for tagging many skills at once (list routes, + * upsert authorization): one workspace-access lookup plus one editor-row scan + * scoped to the workspace. A skill is editable when `canAdminWorkspace` or its + * id is in `editorSkillIds`. + * + * Pass `workspaceAccess` when the caller already resolved it to skip a + * redundant lookup. + */ +export async function getEditableSkillIds( + workspaceId: string, + userId: string, + options?: { workspaceAccess?: WorkspaceAccess } +): Promise { + const [workspaceAccess, editorRows] = await Promise.all([ + resolveWorkspaceAccess(workspaceId, userId, options?.workspaceAccess), + db + .select({ skillId: skillMember.skillId }) + .from(skillMember) + .innerJoin(skill, eq(skillMember.skillId, skill.id)) + .where(and(eq(skill.workspaceId, workspaceId), eq(skillMember.userId, userId))), + ]) + + if (!workspaceAccess.hasAccess) { + return { canAdminWorkspace: false, editorSkillIds: new Set() } + } + + return { + canAdminWorkspace: workspaceAccess.canAdmin, + editorSkillIds: new Set(editorRows.map((row) => row.skillId)), + } +} + +export interface SkillEditor { + /** Explicit row id, or a synthetic `workspace-admin-` id for derived admins without rows. */ + id: string + userId: string + userName: string | null + userEmail: string | null + userImage: string | null + /** Derived editors — always present, cannot be removed from the list. */ + isWorkspaceAdmin: boolean +} + +/** + * The editor roster for a skill: every workspace admin (derived, undemotable) + * plus every explicit-row user still in the workspace roster. Rows for users + * who left the workspace are ignored, exactly as edit enforcement ignores them. + */ +export async function listSkillEditors(skillRow: { + id: string + workspaceId: string +}): Promise { + const [explicitRows, workspaceMembers] = await Promise.all([ + db + .select({ id: skillMember.id, userId: skillMember.userId }) + .from(skillMember) + .where(eq(skillMember.skillId, skillRow.id)), + getUsersWithPermissions(skillRow.workspaceId), + ]) + + const rowByUser = new Map(explicitRows.map((row) => [row.userId, row])) + + const editors: SkillEditor[] = [] + for (const wsMember of workspaceMembers) { + const row = rowByUser.get(wsMember.userId) + const isWorkspaceAdmin = wsMember.permissionType === 'admin' + if (!row && !isWorkspaceAdmin) continue + + editors.push({ + id: row?.id ?? `workspace-admin-${wsMember.userId}`, + userId: wsMember.userId, + userName: wsMember.name, + userEmail: wsMember.email, + userImage: wsMember.image ?? null, + isWorkspaceAdmin, + }) + } + return editors +} + +export interface SkillsUpdateAccess { + /** Ids from the request that resolve to existing skills in the workspace. */ + existingIds: Set + /** Existing skills the user may not update (not an editor, not a workspace admin). */ + denied: Array<{ id: string; name: string }> +} + +/** + * Partitions an upsert request's skill ids for authorization: ids that resolve + * to existing workspace skills require skill editor access; unresolved ids are + * creates, gated by workspace write permission instead. + */ +export async function checkSkillsUpdateAccess(params: { + workspaceId: string + userId: string + skillIds: string[] + workspaceAccess?: WorkspaceAccess +}): Promise { + if (params.skillIds.length === 0) return { existingIds: new Set(), denied: [] } + + const rows = await db + .select({ id: skill.id, name: skill.name }) + .from(skill) + .where(and(eq(skill.workspaceId, params.workspaceId), inArray(skill.id, params.skillIds))) + + const existingIds = new Set(rows.map((row) => row.id)) + if (rows.length === 0) return { existingIds, denied: [] } + + const access = await getEditableSkillIds(params.workspaceId, params.userId, { + workspaceAccess: params.workspaceAccess, + }) + const denied = access.canAdminWorkspace + ? [] + : rows.filter((row) => !access.editorSkillIds.has(row.id)) + + return { existingIds, denied } +} + +/** + * Removes a user's skill editor grants across one or more workspaces when they + * leave (workspace removal, org removal/transfer). Rows are editor grants + * only — everyone in the workspace already sees and uses every skill — so a + * later re-invite lands them with no edit rights until re-added. Workspace + * admins are derived editors, so no promotion is needed to avoid orphaning a + * skill. Returns the number of grants removed. + */ +export async function removeWorkspaceSkillMembershipsTx( + tx: DbOrTx, + workspaceId: string | string[], + userId: string +): Promise { + const workspaceIds = Array.isArray(workspaceId) ? workspaceId : [workspaceId] + if (workspaceIds.length === 0) return 0 + + const removed = await tx + .delete(skillMember) + .where( + and( + eq(skillMember.userId, userId), + inArray( + skillMember.skillId, + tx.select({ id: skill.id }).from(skill).where(inArray(skill.workspaceId, workspaceIds)) + ) + ) + ) + .returning({ id: skillMember.id }) + + return removed.length +} diff --git a/apps/sim/lib/workflows/skills/operations.test.ts b/apps/sim/lib/workflows/skills/operations.test.ts index 9c5ebc167e..62f49c01b5 100644 --- a/apps/sim/lib/workflows/skills/operations.test.ts +++ b/apps/sim/lib/workflows/skills/operations.test.ts @@ -4,10 +4,17 @@ import { dbChainMock, queueTableRows, resetDbChainMock, schemaMock } from '@sim/testing' import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' -vi.mock('@sim/db', () => ({ ...dbChainMock, ...schemaMock })) -vi.mock('@sim/utils/id', () => ({ generateShortId: () => 'gen-id' })) +const { getEditableSkillIdsMock } = vi.hoisted(() => ({ + getEditableSkillIdsMock: vi.fn(), +})) -import { listSkills } from '@/lib/workflows/skills/operations' +vi.mock('@sim/db', () => ({ ...dbChainMock, ...schemaMock })) +vi.mock('@sim/utils/id', () => ({ generateId: () => 'gen-uuid', generateShortId: () => 'gen-id' })) +vi.mock('@/lib/skills/access', () => ({ + getEditableSkillIds: getEditableSkillIdsMock, +})) + +import { listSkills, listSkillsForUser } from '@/lib/workflows/skills/operations' describe('listSkills includeBuiltins', () => { beforeEach(() => { @@ -39,3 +46,76 @@ describe('listSkills includeBuiltins', () => { expect(result.some((s) => s.id.startsWith('builtin-'))).toBe(false) }) }) + +describe('listSkillsForUser', () => { + beforeEach(() => { + vi.clearAllMocks() + resetDbChainMock() + getEditableSkillIdsMock.mockResolvedValue({ + canAdminWorkspace: false, + editorSkillIds: new Set(), + }) + }) + + afterAll(() => { + resetDbChainMock() + }) + + it('returns every workspace skill tagged with edit access from editor rows', async () => { + queueTableRows(schemaMock.skill, [ + { id: 'sk-mine', name: 'mine', description: 'd', content: 'c', workspaceId: 'ws-1' }, + { id: 'sk-other', name: 'other', description: 'd', content: 'c', workspaceId: 'ws-1' }, + ]) + getEditableSkillIdsMock.mockResolvedValue({ + canAdminWorkspace: false, + editorSkillIds: new Set(['sk-mine']), + }) + + const result = await listSkillsForUser({ + workspaceId: 'ws-1', + userId: 'user-1', + includeBuiltins: false, + }) + + expect(result).toHaveLength(2) + expect(result.find((s) => s.id === 'sk-mine')).toMatchObject({ canEdit: true }) + expect(result.find((s) => s.id === 'sk-other')).toMatchObject({ canEdit: false }) + }) + + it('tags every skill editable for workspace admins', async () => { + queueTableRows(schemaMock.skill, [ + { id: 'sk-1', name: 'one', description: 'd', content: 'c', workspaceId: 'ws-1' }, + { id: 'sk-2', name: 'two', description: 'd', content: 'c', workspaceId: 'ws-1' }, + ]) + getEditableSkillIdsMock.mockResolvedValue({ + canAdminWorkspace: true, + editorSkillIds: new Set(), + }) + + const result = await listSkillsForUser({ + workspaceId: 'ws-1', + userId: 'admin-1', + includeBuiltins: false, + }) + + expect(result.every((s) => s.canEdit)).toBe(true) + }) + + it('always passes builtin skills through as non-editable', async () => { + const result = await listSkillsForUser({ workspaceId: 'ws-1', userId: 'user-1' }) + + expect(result.length).toBeGreaterThan(0) + expect(result.every((s) => s.id.startsWith('builtin-') && s.canEdit === false)).toBe(true) + }) + + it('lets a workspace skill sharing a builtin name override it for everyone', async () => { + queueTableRows(schemaMock.skill, [ + { id: 'sk-research', name: 'research', description: 'd', content: 'c', workspaceId: 'ws-1' }, + ]) + + const result = await listSkillsForUser({ workspaceId: 'ws-1', userId: 'user-1' }) + + expect(result.some((s) => s.id === 'sk-research')).toBe(true) + expect(result.some((s) => s.id === 'builtin-research')).toBe(false) + }) +}) diff --git a/apps/sim/lib/workflows/skills/operations.ts b/apps/sim/lib/workflows/skills/operations.ts index 7e0fcfb4c2..4746cc0d5f 100644 --- a/apps/sim/lib/workflows/skills/operations.ts +++ b/apps/sim/lib/workflows/skills/operations.ts @@ -1,15 +1,17 @@ import { db } from '@sim/db' -import { skill } from '@sim/db/schema' +import { skill, skillMember } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { generateShortId } from '@sim/utils/id' +import { generateId, generateShortId } from '@sim/utils/id' import { and, desc, eq, ne } from 'drizzle-orm' import { generateRequestId } from '@/lib/core/utils/request' +import { getEditableSkillIds } from '@/lib/skills/access' import { BUILTIN_SKILLS, type BuiltinSkill, getBuiltinSkillById, isBuiltinSkillId, } from '@/lib/workflows/skills/builtin-skills' +import type { WorkspaceAccess } from '@/lib/workspaces/permissions/utils' const logger = createLogger('SkillsOperations') @@ -57,6 +59,46 @@ export async function listSkills(params: { workspaceId: string; includeBuiltins? return [...builtins, ...dbRows] } +/** A skill row tagged with whether the caller can edit it (always false on builtins). */ +export type SkillWithAccess = typeof skill.$inferSelect & { canEdit: boolean } + +/** + * List every skill in the workspace, each tagged with whether the caller can + * edit it: workspace admins can edit all; others can edit skills where they + * hold an explicit editor row. Everyone in the workspace sees and uses every + * skill. Built-in template skills are code-only and never editable. + * + * Pass `workspaceAccess` when the caller already resolved it to skip a + * redundant lookup. + */ +export async function listSkillsForUser(params: { + workspaceId: string + userId: string + includeBuiltins?: boolean + workspaceAccess?: WorkspaceAccess +}): Promise { + const [dbRows, access] = await Promise.all([ + listSkills({ workspaceId: params.workspaceId, includeBuiltins: false }), + getEditableSkillIds(params.workspaceId, params.userId, { + workspaceAccess: params.workspaceAccess, + }), + ]) + + const tagged: SkillWithAccess[] = dbRows.map((row) => ({ + ...row, + canEdit: access.canAdminWorkspace || access.editorSkillIds.has(row.id), + })) + + if (params.includeBuiltins === false) return tagged + + // A workspace skill that shares a built-in's name overrides it for everyone. + const dbNames = new Set(tagged.map((r) => r.name.toLowerCase())) + const builtins: SkillWithAccess[] = BUILTIN_SKILLS.filter( + (b) => !dbNames.has(b.name.toLowerCase()) + ).map((b) => ({ ...builtinSkillRow(params.workspaceId, b), canEdit: false })) + return [...builtins, ...tagged] +} + /** * Fetch a single skill by id, scoped to a workspace. Built-in template skills * resolve from code; otherwise returns the DB row, or null when the skill does @@ -112,7 +154,11 @@ export interface TouchedSkill { } export interface UpsertSkillsResult { - /** Every skill in the workspace after the upsert, ordered by createdAt desc. */ + /** + * Every skill in the workspace after the upsert, ordered by createdAt desc. + * Empty when `returnSkills: false` — callers that re-fetch a filtered list + * themselves opt out so the transaction never re-reads full content bodies. + */ skills: Awaited> /** Only the skills this upsert created or updated, tagged with the operation. */ touched: TouchedSkill[] @@ -125,13 +171,14 @@ export interface UpsertSkillsResult { export async function upsertSkills(params: { skills: Array<{ id?: string - name: string - description: string - content: string + name?: string + description?: string + content?: string }> workspaceId: string userId: string requestId?: string + returnSkills?: boolean }): Promise { const { skills, workspaceId, userId, requestId = generateRequestId() } = params @@ -147,41 +194,53 @@ export async function upsertSkills(params: { const nowTime = new Date() if (s.id) { - const existingSkill = await tx + // Id-carrying items are updates and never fall through to a create: the + // caller's authorization partitioned on resolvability, so a vanished id + // must surface as not-found rather than an ungated (re-)create. + const [current] = await tx .select() .from(skill) .where(and(eq(skill.id, s.id), eq(skill.workspaceId, workspaceId))) .limit(1) - if (existingSkill.length > 0) { - if (s.name !== existingSkill[0].name) { - const nameConflict = await tx - .select({ id: skill.id }) - .from(skill) - .where( - and(eq(skill.workspaceId, workspaceId), eq(skill.name, s.name), ne(skill.id, s.id)) - ) - .limit(1) - - if (nameConflict.length > 0) { - throw new Error(`A skill with the name "${s.name}" already exists in this workspace`) - } - } - - await tx - .update(skill) - .set({ - name: s.name, - description: s.description, - content: s.content, - updatedAt: nowTime, - }) - .where(and(eq(skill.id, s.id), eq(skill.workspaceId, workspaceId))) - - touched.push({ id: s.id, name: s.name, operation: 'updated' }) - logger.info(`[${requestId}] Updated skill ${s.id}`) - continue + if (!current) { + throw new Error(`Skill not found: ${s.id}`) } + + // Partial update: omitted fields keep their current values, so a + // sharing-only toggle can never clobber a concurrent content edit. + const nextName = s.name ?? current.name + if (nextName !== current.name) { + const nameConflict = await tx + .select({ id: skill.id }) + .from(skill) + .where( + and(eq(skill.workspaceId, workspaceId), eq(skill.name, nextName), ne(skill.id, s.id)) + ) + .limit(1) + + if (nameConflict.length > 0) { + throw new Error(`The skill name "${nextName}" is unavailable in this workspace`) + } + } + + await tx + .update(skill) + .set({ + name: nextName, + description: s.description ?? current.description, + content: s.content ?? current.content, + updatedAt: nowTime, + }) + .where(and(eq(skill.id, s.id), eq(skill.workspaceId, workspaceId))) + + touched.push({ id: s.id, name: nextName, operation: 'updated' }) + logger.info(`[${requestId}] Updated skill ${s.id}`) + continue + } + + if (!s.name || !s.description || !s.content) { + throw new Error('Skill name, description, and content are required to create a skill') } const duplicateName = await tx @@ -191,7 +250,7 @@ export async function upsertSkills(params: { .limit(1) if (duplicateName.length > 0) { - throw new Error(`A skill with the name "${s.name}" already exists in this workspace`) + throw new Error(`The skill name "${s.name}" is unavailable in this workspace`) } const newId = generateShortId() @@ -206,10 +265,25 @@ export async function upsertSkills(params: { updatedAt: nowTime, }) + // The creator becomes an editor; workspace admins are derived editors + // with no rows, and everyone in the workspace can already use the skill. + await tx.insert(skillMember).values({ + id: generateId(), + skillId: newId, + userId, + invitedBy: userId, + createdAt: nowTime, + updatedAt: nowTime, + }) + touched.push({ id: newId, name: s.name, operation: 'created' }) logger.info(`[${requestId}] Created skill "${s.name}"`) } + if (params.returnSkills === false) { + return { skills: [], touched } + } + const resultSkills = await tx .select() .from(skill) diff --git a/apps/sim/lib/workspaces/permissions/utils.ts b/apps/sim/lib/workspaces/permissions/utils.ts index 81c12739b4..6afc4f6655 100644 --- a/apps/sim/lib/workspaces/permissions/utils.ts +++ b/apps/sim/lib/workspaces/permissions/utils.ts @@ -164,6 +164,21 @@ export async function checkWorkspaceAccess( return { exists: true, hasAccess, canWrite, canAdmin, workspace: ws, permission } } +/** + * Returns `provided` when it was resolved for this exact workspace, otherwise + * resolves fresh. The id match is what keeps a caller from authorizing against + * another workspace's cached access - every access-reuse path must go through + * this rather than hand-rolling the comparison. + */ +export async function resolveWorkspaceAccess( + workspaceId: string, + userId: string, + provided?: WorkspaceAccess +): Promise { + if (provided && provided.workspace?.id === workspaceId) return provided + return checkWorkspaceAccess(workspaceId, userId) +} + /** * Thrown when a user attempts to access a workspace they don't have access to, * or that doesn't exist / has been archived. Carries `statusCode = 403` so the diff --git a/apps/sim/app/workspace/[workspaceId]/components/credential-detail/sharing.test.ts b/apps/sim/lib/workspaces/sharing.test.ts similarity index 96% rename from apps/sim/app/workspace/[workspaceId]/components/credential-detail/sharing.test.ts rename to apps/sim/lib/workspaces/sharing.test.ts index c762ac9f0f..1c20268409 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/credential-detail/sharing.test.ts +++ b/apps/sim/lib/workspaces/sharing.test.ts @@ -2,7 +2,7 @@ * @vitest-environment node */ import { describe, expect, it } from 'vitest' -import { partitionSettledFailures, resolveAddEmail } from './sharing' +import { partitionSettledFailures, resolveAddEmail } from '@/lib/workspaces/sharing' describe('resolveAddEmail', () => { const ctx = { diff --git a/apps/sim/app/workspace/[workspaceId]/components/credential-detail/sharing.ts b/apps/sim/lib/workspaces/sharing.ts similarity index 88% rename from apps/sim/app/workspace/[workspaceId]/components/credential-detail/sharing.ts rename to apps/sim/lib/workspaces/sharing.ts index f315ba0eed..a42eed3bd2 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/credential-detail/sharing.ts +++ b/apps/sim/lib/workspaces/sharing.ts @@ -1,14 +1,15 @@ export interface ResolveAddEmailContext { /** Lowercased email -> workspace userId for every workspace member. */ workspaceUserIdByEmail: Map - /** Lowercased emails that already have access to the credential. */ + /** Lowercased emails that already have access to the resource. */ existingMemberEmails: Set } export type ResolveAddEmailResult = { userId: string } | { error: string } /** - * Decide whether a format-valid email can be added to a credential: it must + * Decide whether a format-valid email can be added to a shared resource + * (credential, skill): it must * belong to a workspace member and not already have access. Matching is * case-insensitive (the context map/set are keyed by lowercased email) while * error messages echo the email as the user typed it. Returns the resolved diff --git a/bun.lock b/bun.lock index 69f9acb71a..849269e079 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "simstudio", @@ -601,6 +602,7 @@ "postgres": "^3.4.5", "react": "19.2.4", "react-dom": "19.2.4", + "zod": "4.3.6", }, "packages": { "@1password/sdk": ["@1password/sdk@0.3.1", "", { "dependencies": { "@1password/sdk-core": "0.3.1" } }, "sha512-20zbQfqsjcECT0gvnAw4zONJDt3XQgNH946pZR0NV1Qxukyaz/DKB0cBnBNCCEWZg93Bah8poaR6gJCyuNX14w=="], @@ -3095,7 +3097,7 @@ "lru.min": ["lru.min@1.1.4", "", {}, "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA=="], - "lucide-react": ["lucide-react@1.23.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-38BpJcD0JhFosxHApP/BYsBetLpQFRoTRzEzstM/XCc3jsAG7wqaY1lgVwxiUe3xqYE+lNxo2PkCmYwXWrwwIw=="], + "lucide-react": ["lucide-react@0.479.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-aBhNnveRhorBOK7uA4gDjgaf+YlHMdMhQ/3cupk6exM10hWlEU+2QtWYOfhXhjAsmdb6LeKR+NZnow4UxRRiTQ=="], "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], @@ -4211,16 +4213,10 @@ "@better-auth/core/jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="], - "@better-auth/core/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], - "@better-auth/sso/jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="], "@better-auth/sso/tldts": ["tldts@6.1.86", "", { "dependencies": { "tldts-core": "^6.1.86" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ=="], - "@better-auth/sso/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], - - "@better-auth/stripe/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], - "@browserbasehq/sdk/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], "@browserbasehq/sdk/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], @@ -4535,10 +4531,6 @@ "@sim/db/@types/node": ["@types/node@22.19.21", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-VMeFBSCKQKmm2swI2kW51SFusDqekC6q9trBCvJ/JliDchFSuoYYKN7yVNjPthP1HKZcx3U1gI/wTcEBjEFKTA=="], - "@sim/emcn/lucide-react": ["lucide-react@0.479.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-aBhNnveRhorBOK7uA4gDjgaf+YlHMdMhQ/3cupk6exM10hWlEU+2QtWYOfhXhjAsmdb6LeKR+NZnow4UxRRiTQ=="], - - "@sim/workflow-renderer/lucide-react": ["lucide-react@0.479.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-aBhNnveRhorBOK7uA4gDjgaf+YlHMdMhQ/3cupk6exM10hWlEU+2QtWYOfhXhjAsmdb6LeKR+NZnow4UxRRiTQ=="], - "@smithy/middleware-compression/fflate": ["fflate@0.8.1", "", {}, "sha512-/exOvEuc+/iaUm105QIiOt4LpBdMTWsXxqR0HDF35vx3fmaKzw7354gTilCh5rkzEt8WYyG//ku3h3nRmd7CHQ=="], "@socket.io/redis-adapter/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], @@ -4595,8 +4587,6 @@ "@trigger.dev/core/tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], - "@trigger.dev/core/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "@trigger.dev/sdk/@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], "@trigger.dev/sdk/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.36.0", "", {}, "sha512-TtxJSRD8Ohxp6bKkhrm27JRHAxPczQA7idtcTOMYI+wQRRrfgqxHv1cFbCApcSnNjtXkmzFozn6jQtFrOmbjPQ=="], @@ -4635,8 +4625,6 @@ "better-auth/jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="], - "better-auth/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], - "bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], "bl/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], @@ -4733,18 +4721,20 @@ "fumadocs-mdx/js-yaml": ["js-yaml@4.2.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw=="], - "fumadocs-mdx/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], - "fumadocs-openapi/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], "fumadocs-openapi/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], "fumadocs-openapi/js-yaml": ["js-yaml@4.2.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw=="], + "fumadocs-openapi/lucide-react": ["lucide-react@1.23.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-38BpJcD0JhFosxHApP/BYsBetLpQFRoTRzEzstM/XCc3jsAG7wqaY1lgVwxiUe3xqYE+lNxo2PkCmYwXWrwwIw=="], + "fumadocs-openapi/tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="], "fumadocs-ui/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], + "fumadocs-ui/lucide-react": ["lucide-react@1.23.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-38BpJcD0JhFosxHApP/BYsBetLpQFRoTRzEzstM/XCc3jsAG7wqaY1lgVwxiUe3xqYE+lNxo2PkCmYwXWrwwIw=="], + "fumadocs-ui/tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="], "gaxios/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], @@ -4903,8 +4893,6 @@ "sharp/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], - "sim/lucide-react": ["lucide-react@0.479.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-aBhNnveRhorBOK7uA4gDjgaf+YlHMdMhQ/3cupk6exM10hWlEU+2QtWYOfhXhjAsmdb6LeKR+NZnow4UxRRiTQ=="], - "sim/tailwindcss": ["tailwindcss@3.4.19", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.21.7", "lilconfig": "^3.1.3", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", "postcss-nested": "^6.2.0", "postcss-selector-parser": "^6.1.2", "resolve": "^1.22.8", "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ=="], "simstudio/@types/node": ["@types/node@20.19.43", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA=="], @@ -4969,10 +4957,6 @@ "xml2js/xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="], - "zod-error/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - - "zod-validation-error/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "zrender/tslib": ["tslib@2.3.0", "", {}, "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="], "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], diff --git a/package.json b/package.json index 374ffb1f23..95b4486ff5 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,8 @@ "drizzle-orm": "^0.45.2", "postgres": "^3.4.5", "minimatch": "^10.2.5", - "mermaid": "11.15.0" + "mermaid": "11.15.0", + "zod": "4.3.6" }, "devDependencies": { "@biomejs/biome": "2.0.0-beta.5", diff --git a/packages/audit/src/types.ts b/packages/audit/src/types.ts index 90e02afe2a..a7bfb98040 100644 --- a/packages/audit/src/types.ts +++ b/packages/audit/src/types.ts @@ -159,6 +159,8 @@ export const AuditAction = { SKILL_CREATED: 'skill.created', SKILL_UPDATED: 'skill.updated', SKILL_DELETED: 'skill.deleted', + SKILL_MEMBER_ADDED: 'skill_member.added', + SKILL_MEMBER_REMOVED: 'skill_member.removed', // Schedules SCHEDULE_CREATED: 'schedule.created', diff --git a/packages/db/migrations/0267_dark_romulus.sql b/packages/db/migrations/0267_dark_romulus.sql new file mode 100644 index 0000000000..b75860a6ee --- /dev/null +++ b/packages/db/migrations/0267_dark_romulus.sql @@ -0,0 +1,29 @@ +CREATE TABLE "skill_member" ( + "id" text PRIMARY KEY NOT NULL, + "skill_id" text NOT NULL, + "user_id" text NOT NULL, + "invited_by" text, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "skill_member" ADD CONSTRAINT "skill_member_skill_id_skill_id_fk" FOREIGN KEY ("skill_id") REFERENCES "public"."skill"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "skill_member" ADD CONSTRAINT "skill_member_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "skill_member" ADD CONSTRAINT "skill_member_invited_by_user_id_fk" FOREIGN KEY ("invited_by") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "skill_member_user_id_idx" ON "skill_member" USING btree ("user_id");--> statement-breakpoint +CREATE UNIQUE INDEX "skill_member_unique" ON "skill_member" USING btree ("skill_id","user_id");--> statement-breakpoint +-- Backfill: existing skills predate the editors list, where any workspace `write` user +-- could edit any skill. Grant those users an explicit editor row so their edit rights +-- survive the cutover. Workspace admins get no rows (they are derived editors at +-- runtime), which the write-only permission join guarantees — the permissions table is +-- unique on (user_id, entity_type, entity_id). Everyone with workspace access can see +-- and use every skill regardless of rows, so no other grants are needed. +-- Idempotent and deploy-window re-runnable: deterministic ids + ON CONFLICT DO NOTHING, +-- so re-running this INSERT once after full cutover heals any skill created by a +-- still-running old pod during the deploy window. +INSERT INTO "skill_member" ("id", "skill_id", "user_id", "invited_by", "created_at", "updated_at") +SELECT 'skillm_' || md5(s."id" || ':' || p."user_id"), s."id", p."user_id", s."user_id", now(), now() +FROM "skill" s +INNER JOIN "permissions" p ON p."entity_type" = 'workspace' AND p."entity_id" = s."workspace_id" AND p."permission_type" = 'write' +WHERE s."workspace_id" IS NOT NULL +ON CONFLICT DO NOTHING; \ No newline at end of file diff --git a/packages/db/migrations/meta/0267_snapshot.json b/packages/db/migrations/meta/0267_snapshot.json new file mode 100644 index 0000000000..4be63a659f --- /dev/null +++ b/packages/db/migrations/meta/0267_snapshot.json @@ -0,0 +1,17529 @@ +{ + "id": "25435972-1fe1-4766-a728-d8e5c8818168", + "prevId": "dd2e88e1-ce36-426e-9857-a9f8ab6141fb", + "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_number_idx": { + "name": "academy_certificate_number_idx", + "columns": [ + { + "expression": "certificate_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "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 + }, + "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": {} + } + }, + "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 + }, + "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 + }, + "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_tool_call_id_idx": { + "name": "copilot_async_tool_calls_tool_call_id_idx", + "columns": [ + { + "expression": "tool_call_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 + }, + "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 + }, + "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": "'[]'" + }, + "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_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" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "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": true + }, + "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 + }, + "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 + }, + "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": { + "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_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": {} + } + }, + "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_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_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_oauth_source_check": { + "name": "credential_oauth_source_check", + "value": "(type <> 'oauth') OR (account_id IS NOT NULL AND provider_id 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_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 + }, + "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_started_at": { + "name": "processing_started_at", + "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 + }, + "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 + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "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_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_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_id_idx": { + "name": "doc_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "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_tag1_idx": { + "name": "doc_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag2_idx": { + "name": "doc_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag3_idx": { + "name": "doc_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag4_idx": { + "name": "doc_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag5_idx": { + "name": "doc_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag6_idx": { + "name": "doc_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag7_idx": { + "name": "doc_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": 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": {}, + "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 + }, + "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_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 + } + }, + "emb_tag1_idx": { + "name": "emb_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag2_idx": { + "name": "emb_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag3_idx": { + "name": "emb_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag4_idx": { + "name": "emb_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag5_idx": { + "name": "emb_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag6_idx": { + "name": "emb_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag7_idx": { + "name": "emb_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": 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_not_null_check": { + "name": "embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + } + }, + "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": {} + } + }, + "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": {} + } + }, + "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.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 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": 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_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_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": {} + } + }, + "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" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "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 + }, + "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 + }, + "next_sync_at": { + "name": "next_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "consecutive_failures": { + "name": "consecutive_failures", + "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()" + }, + "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": {} + } + }, + "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" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "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_failed": { + "name": "docs_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kcsl_connector_id_idx": { + "name": "kcsl_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "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.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": true + }, + "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" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "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": true + }, + "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_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_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_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": {}, + "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 + }, + "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.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": { + "mothership_settings_workspace_id_idx": { + "name": "mothership_settings_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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.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_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.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_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": true + }, + "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 + }, + "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_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_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": {}, + "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 + } + }, + "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_user_entity_idx": { + "name": "permissions_user_entity_idx", + "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": 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.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 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "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.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": {} + }, + "session_token_idx": { + "name": "session_token_idx", + "columns": [ + { + "expression": "token", + "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 + }, + "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.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 + } + }, + "indexes": { + "sso_provider_provider_id_idx": { + "name": "sso_provider_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "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 + } + }, + "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": {} + } + }, + "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 + }, + "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" + } + }, + "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 + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "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": {} + } + }, + "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" + } + }, + "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_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 + } + }, + "indexes": {}, + "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 + }, + "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 + }, + "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_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_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_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 + }, + "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_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": {} + }, + "webhook_tiktok_credential_id_idx": { + "name": "webhook_tiktok_credential_id_idx", + "columns": [ + { + "expression": "((\"provider_config\")::jsonb ->> 'credentialId')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook\".\"provider\" = 'tiktok' AND \"webhook\".\"is_active\" = true AND \"webhook\".\"archived_at\" IS NULL", + "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_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_workflow_folder_id_fk": { + "name": "workflow_folder_id_workflow_folder_id_fk", + "tableFrom": "workflow", + "tableTo": "workflow_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 + }, + "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 + }, + "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_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_folder": { + "name": "workflow_folder", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "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 + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'#6B7280'" + }, + "is_expanded": { + "name": "is_expanded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "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()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_folder_user_idx": { + "name": "workflow_folder_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_parent_idx": { + "name": "workflow_folder_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_parent_sort_idx": { + "name": "workflow_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": {} + }, + "workflow_folder_archived_at_idx": { + "name": "workflow_folder_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_archived_partial_idx": { + "name": "workflow_folder_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_folder\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_folder_user_id_user_id_fk": { + "name": "workflow_folder_user_id_user_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_workspace_id_workspace_id_fk": { + "name": "workflow_folder_workspace_id_workspace_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "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 + }, + "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 + }, + "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": {} + } + }, + "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_key_idx": { + "name": "workspace_file_key_idx", + "columns": [ + { + "expression": "key", + "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_folders": { + "name": "workspace_file_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "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 + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "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": { + "workspace_file_folders_workspace_parent_idx": { + "name": "workspace_file_folders_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_parent_sort_idx": { + "name": "workspace_file_folders_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": {} + }, + "workspace_file_folders_deleted_at_idx": { + "name": "workspace_file_folders_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_workspace_deleted_partial_idx": { + "name": "workspace_file_folders_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_folders\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_workspace_parent_name_active_unique": { + "name": "workspace_file_folders_workspace_parent_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "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": "\"workspace_file_folders\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_folders_user_id_user_id_fk": { + "name": "workspace_file_folders_user_id_user_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_folders_workspace_id_workspace_id_fk": { + "name": "workspace_file_folders_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_folders_parent_id_workspace_file_folders_id_fk": { + "name": "workspace_file_folders_parent_id_workspace_file_folders_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "workspace_file_folders", + "columnsFrom": ["parent_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "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 + }, + "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 + }, + "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()" + } + }, + "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_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_folder_id_workspace_file_folders_id_fk": { + "name": "workspace_files_folder_id_workspace_file_folders_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace_file_folders", + "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": {}, + "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 + } + }, + "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.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", "env_workspace", "env_personal", "service_account"] + }, + "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.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.permission_type": { + "name": "permission_type", + "schema": "public", + "values": ["admin", "write", "read"] + }, + "public.usage_log_category": { + "name": "usage_log_category", + "schema": "public", + "values": ["model", "fixed", "tool"] + }, + "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" + ] + }, + "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", + "mcp_server", + "workflow_mcp_server", + "custom_tool", + "skill" + ] + }, + "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 39b5fac75a..7ec307a7e7 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -1863,6 +1863,13 @@ "when": 1784838122475, "tag": "0266_spotty_alex_power", "breakpoints": true + }, + { + "idx": 267, + "version": "7", + "when": 1784861110986, + "tag": "0267_dark_romulus", + "breakpoints": true } ] } diff --git a/packages/db/schema.ts b/packages/db/schema.ts index 146284517b..2badf30569 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -1050,6 +1050,31 @@ export const skill = pgTable( }) ) +/** + * Editor grants for a skill. A row makes the user an editor (edit, delete, + * share); workspace admins are derived editors and need no rows. Everyone with + * workspace access can see and use every skill regardless of rows. + */ +export const skillMember = pgTable( + 'skill_member', + { + id: text('id').primaryKey(), + skillId: text('skill_id') + .notNull() + .references(() => skill.id, { onDelete: 'cascade' }), + userId: text('user_id') + .notNull() + .references(() => user.id, { onDelete: 'cascade' }), + invitedBy: text('invited_by').references(() => user.id, { onDelete: 'set null' }), + createdAt: timestamp('created_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at').notNull().defaultNow(), + }, + (table) => ({ + userIdIdx: index('skill_member_user_id_idx').on(table.userId), + uniqueMembership: uniqueIndex('skill_member_unique').on(table.skillId, table.userId), + }) +) + export const mothershipSettings = pgTable( 'mothership_settings', { diff --git a/packages/emcn/src/components/chip-switch/chip-switch.tsx b/packages/emcn/src/components/chip-switch/chip-switch.tsx index 717af940ba..cf0cf535de 100644 --- a/packages/emcn/src/components/chip-switch/chip-switch.tsx +++ b/packages/emcn/src/components/chip-switch/chip-switch.tsx @@ -22,7 +22,7 @@ export interface ChipSwitchOption { */ export interface ChipSwitchProps { /** Ordered list of options to render as segments. */ - options: ChipSwitchOption[] + options: readonly ChipSwitchOption[] /** Currently selected value. */ value: T /** Invoked with the next selection when a segment is clicked. */ diff --git a/packages/testing/src/mocks/audit.mock.ts b/packages/testing/src/mocks/audit.mock.ts index e18095f5d3..356ebc4174 100644 --- a/packages/testing/src/mocks/audit.mock.ts +++ b/packages/testing/src/mocks/audit.mock.ts @@ -131,6 +131,8 @@ export const auditMock = { SKILL_CREATED: 'skill.created', SKILL_UPDATED: 'skill.updated', SKILL_DELETED: 'skill.deleted', + SKILL_MEMBER_ADDED: 'skill_member.added', + SKILL_MEMBER_REMOVED: 'skill_member.removed', TABLE_CREATED: 'table.created', TABLE_UPDATED: 'table.updated', TABLE_DELETED: 'table.deleted', diff --git a/packages/testing/src/mocks/schema.mock.ts b/packages/testing/src/mocks/schema.mock.ts index 9f892b1c2e..f7f4084c79 100644 --- a/packages/testing/src/mocks/schema.mock.ts +++ b/packages/testing/src/mocks/schema.mock.ts @@ -406,6 +406,14 @@ export const schemaMock = { createdAt: 'createdAt', updatedAt: 'updatedAt', }, + skillMember: { + id: 'id', + skillId: 'skillId', + userId: 'userId', + invitedBy: 'invitedBy', + createdAt: 'createdAt', + updatedAt: 'updatedAt', + }, subscription: { id: 'id', plan: 'plan', diff --git a/scripts/check-api-validation-contracts.ts b/scripts/check-api-validation-contracts.ts index 8289bba83d..b6f582a143 100644 --- a/scripts/check-api-validation-contracts.ts +++ b/scripts/check-api-validation-contracts.ts @@ -9,8 +9,8 @@ const QUERY_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/queries') const SELECTOR_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/selectors') const BASELINE = { - totalRoutes: 969, - zodRoutes: 969, + totalRoutes: 970, + zodRoutes: 970, nonZodRoutes: 0, } as const diff --git a/scripts/check-migrations-safety.ts b/scripts/check-migrations-safety.ts index 5c4e299af2..5008b48e36 100644 --- a/scripts/check-migrations-safety.ts +++ b/scripts/check-migrations-safety.ts @@ -25,6 +25,7 @@ * bun run scripts/check-migrations-safety.ts --dir # a directory */ import { execFileSync } from 'node:child_process' +import { existsSync } from 'node:fs' import { readdir, readFile } from 'node:fs/promises' import path from 'node:path' @@ -414,7 +415,9 @@ function changedMigrationFiles(baseRef: string): string[] { if (inDir(p)) files.add(p) } } - return [...files] + // A migration deleted in the working tree (e.g. regenerated before commit) has + // no SQL left to lint — skip it rather than crash on the read. + return [...files].filter((f) => existsSync(path.join(ROOT, f))) } async function listSqlFiles(dir: string): Promise {