diff --git a/apps/docs/content/docs/en/enterprise/access-control.mdx b/apps/docs/content/docs/en/enterprise/access-control.mdx index 2e07b677ff3..b1ab31105c9 100644 --- a/apps/docs/content/docs/en/enterprise/access-control.mdx +++ b/apps/docs/content/docs/en/enterprise/access-control.mdx @@ -7,13 +7,13 @@ import { Callout } from 'fumadocs-ui/components/callout' import { FAQ } from '@/components/ui/faq' import { Image } from '@/components/ui/image' -Access Control lets organization admins define permission groups that restrict what each set of users can do — which AI model providers they can use, which workflow blocks they can place, and which platform features are visible to them. Restrictions are enforced both in the workflow executor and in Mothership. +Access Control lets workspace admins define permission groups that restrict what each set of workspace members can do — which AI model providers they can use, which workflow blocks they can place, and which platform features are visible to them. Permission groups are scoped to a single workspace: a user can be in different groups (or no group) in different workspaces. Restrictions are enforced both in the workflow executor and in Mothership, based on the workflow's workspace. --- ## How it works -Access control is built around **permission groups**. Each group has a name, an optional description, and a configuration that defines what its members can and cannot do. A user can belong to at most one permission group at a time. +Access control is built around **permission groups**. Each group belongs to a specific workspace and has a name, an optional description, and a configuration that defines what its members can and cannot do. A user can belong to at most one permission group **per workspace**, but can belong to different groups in different workspaces. When a user runs a workflow or uses Mothership, Sim reads their group's configuration and applies it: @@ -26,13 +26,13 @@ When a user runs a workflow or uses Mothership, Sim reads their group's configur ### 1. Open Access Control settings -Go to **Settings → Enterprise → Access Control** in your workspace. +Go to **Settings → Enterprise → Access Control** in the workspace you want to manage. Each workspace has its own set of permission groups. Access Control settings showing a list of permission groups: Contractors, Sales, Engineering, and Marketing, each with Details and Delete actions ### 2. Create a permission group -Click **+ Create** and enter a name (required) and optional description. You can also enable **Auto-add new members** — when active, any new member who joins the organization is automatically added to this group. Only one group per organization can have this setting enabled at a time. +Click **+ Create** and enter a name (required) and optional description. You can also enable **Auto-add new members** — when active, any new member who joins this workspace is automatically added to this group. Only one group per workspace can have this setting enabled at a time. ### 3. Configure permissions @@ -128,7 +128,7 @@ Controls visibility of platform features and modules. ### 4. Add members -Open the group's **Details** view and add members by searching for users by name or email. Users can only belong to one group at a time — adding a user to a new group removes them from their current one. +Open the group's **Details** view and add members by searching for users by name or email. Only users who already have workspace-level access can be added. A user can only belong to one group per workspace — adding a user to a new group within the same workspace removes them from their current group for that workspace. --- @@ -155,17 +155,17 @@ When a user opens Mothership, their permission group is read before any block or ## User membership rules -- A user can belong to **at most one** permission group at a time. -- Moving a user to a new group automatically removes them from their current group. -- Users not assigned to any group have no restrictions applied (all blocks, providers, and features are available to them). -- If **Auto-add new members** is enabled on a group, new organization members are automatically placed in that group. Only one group per organization can have this setting active. +- A user can belong to **at most one** permission group **per workspace**, but may be in different groups across different workspaces. +- Moving a user to a new group within a workspace automatically removes them from their previous group in that workspace. +- Users not assigned to any group in a workspace have no restrictions applied in that workspace (all blocks, providers, and features are available to them there). +- If **Auto-add new members** is enabled on a group, new members of that workspace are automatically placed in the group. Only one group per workspace can have this setting active. --- diff --git a/apps/docs/content/docs/en/enterprise/index.mdx b/apps/docs/content/docs/en/enterprise/index.mdx index 4484e719051..a4a7aff19bf 100644 --- a/apps/docs/content/docs/en/enterprise/index.mdx +++ b/apps/docs/content/docs/en/enterprise/index.mdx @@ -12,7 +12,7 @@ Sim Enterprise provides advanced features for organizations with enhanced securi ## Access Control -Define permission groups to control what features and integrations team members can use. +Define permission groups on a workspace to control what features and integrations its members can use. Permission groups are scoped to a single workspace — a user can belong to different groups (or no group) in different workspaces. ### Features @@ -22,12 +22,12 @@ Define permission groups to control what features and integrations team members ### Setup -1. Navigate to **Settings** → **Access Control** in your workspace +1. Navigate to **Settings** → **Access Control** in the workspace you want to manage 2. Create a permission group with your desired restrictions -3. Add team members to the permission group +3. Add workspace members to the permission group - Users not assigned to any permission group have full access. Permission restrictions are enforced at both UI and execution time. + Any workspace admin on an Enterprise-entitled workspace can manage permission groups. Users not assigned to any group have full access. Permission restrictions are enforced at both UI and execution time, and apply to workflows based on the workflow's workspace. --- @@ -93,7 +93,7 @@ curl -X DELETE "https://your-instance/api/v1/admin/workspaces/{workspaceId}/memb ### Notes -- Enabling `ACCESS_CONTROL_ENABLED` automatically enables organizations, as access control requires organization membership. +- Access Control is scoped per workspace. Set `ACCESS_CONTROL_ENABLED` and `NEXT_PUBLIC_ACCESS_CONTROL_ENABLED` to enable it on every workspace in a self-hosted deployment, bypassing the Enterprise plan check. - When `DISABLE_INVITATIONS` is set, users cannot send invitations. Use the Admin API to manage workspace and organization memberships instead. diff --git a/apps/docs/content/docs/en/mcp/index.mdx b/apps/docs/content/docs/en/mcp/index.mdx index 7ab67e4880f..50b731e0d26 100644 --- a/apps/docs/content/docs/en/mcp/index.mdx +++ b/apps/docs/content/docs/en/mcp/index.mdx @@ -220,6 +220,6 @@ import { FAQ } from '@/components/ui/faq' { question: "Who can configure MCP servers in a workspace?", answer: "Users with Write permission can configure (add and update) MCP servers in workspace settings. Only Admin permission is required to delete MCP servers. Users with Read permission can view available MCP tools and execute them in agents and MCP Tool blocks. This means all workspace members with at least Read access can use MCP tools in their workflows." }, { question: "Can I use MCP servers from multiple workspaces?", answer: "MCP servers are configured per workspace. Each workspace maintains its own set of MCP server connections. If you need the same MCP server in multiple workspaces, you need to configure it separately in each workspace's settings." }, { question: "How do I update MCP tool schemas after a server changes its available tools?", answer: "Click the Refresh button on the MCP server in your workspace settings. This fetches the latest tool schemas from the server and automatically updates any agent blocks that use those tools with the new parameter definitions." }, - { question: "Can permission groups restrict access to MCP tools?", answer: "Yes. Organization admins can create permission groups that disable MCP tools for specific members using the disableMcpTools configuration option. When this is enabled, affected users will not be able to add or use MCP tools in their workflows." }, + { question: "Can permission groups restrict access to MCP tools?", answer: "Yes. On Enterprise-entitled workspaces, any workspace admin can create a permission group that disables MCP tools for its members using the disableMcpTools option. When this is enabled, affected users will not be able to add or use MCP tools in workflows that belong to that workspace." }, { question: "What happens if an MCP server goes offline during workflow execution?", answer: "If the MCP server is unreachable during execution, the tool call will fail and return an error. In an Agent block, the AI may attempt to handle the failure gracefully. In a standalone MCP Tool block, the workflow step will fail. Check MCP server logs and verify the server is running and accessible to troubleshoot connectivity issues." }, ]} /> \ No newline at end of file diff --git a/apps/docs/content/docs/en/permissions/roles-and-permissions.mdx b/apps/docs/content/docs/en/permissions/roles-and-permissions.mdx index d64cca35f81..fb048651eab 100644 --- a/apps/docs/content/docs/en/permissions/roles-and-permissions.mdx +++ b/apps/docs/content/docs/en/permissions/roles-and-permissions.mdx @@ -202,9 +202,9 @@ import { FAQ } from '@/components/ui/faq' { question: "What is the difference between organization roles and workspace permissions?", answer: "Organization roles (Owner, Admin, or Member) control who can manage the organization itself, including inviting people, creating shared workspaces, and handling billing. Workspace permissions (Read, Write, Admin) control what a user can do within a specific workspace, such as viewing, editing, or managing workflows. A user needs both an organization role and a workspace permission to work within a shared workspace." }, { question: "How many workspaces can I create?", answer: "Free users get 1 personal workspace. Pro users get up to 3 personal workspaces. Max users get up to 10 personal workspaces. Team and Enterprise plans support unlimited shared workspaces under the organization — new invites are gated by your seat count." }, { question: "What happens to my shared workspaces if I cancel or downgrade my Team plan?", answer: "Existing shared workspaces remain accessible to current members, but new invitations are disabled until you upgrade back to a Team or Enterprise plan. No workspaces or members are deleted — the organization is simply dormant until billing is re-enabled." }, - { question: "Can I restrict which integrations or model providers a team member can use?", answer: "Yes. Organization admins can create permission groups with fine-grained controls, including restricting allowed integrations and allowed model providers to specific lists. You can also disable access to MCP tools, custom tools, skills, and various platform features like the knowledge base, API keys, or Copilot on a per-group basis." }, + { question: "Can I restrict which integrations or model providers a team member can use?", answer: "Yes, on Enterprise-entitled workspaces. Any workspace admin can create permission groups with fine-grained controls, including restricting allowed integrations and allowed model providers to specific lists. You can also disable access to MCP tools, custom tools, skills, and various platform features like the knowledge base, API keys, or Copilot on a per-group basis. Permission groups are scoped per workspace — a user can belong to different groups in different workspaces." }, { question: "What happens when a personal environment variable has the same name as a workspace variable?", answer: "The personal environment variable takes priority. When a workflow runs, if both a personal and workspace variable share the same name, the personal value is used. This allows individual users to override shared workspace configuration when needed." }, { question: "Can an Admin remove the workspace owner?", answer: "No. The workspace owner cannot be removed from the workspace by anyone. Only the workspace owner can delete the workspace or transfer ownership to another user. Admins can do everything else, including inviting and removing other users and managing workspace settings." }, - { question: "What are permission groups and how do they work?", answer: "Permission groups are an advanced access control feature that lets organization admins define granular restrictions beyond the standard Read/Write/Admin roles. A permission group can hide UI sections (like trace spans, knowledge base, API keys, or deployment options), disable features (MCP tools, custom tools, skills, invitations), and restrict which integrations and model providers members can access. Members can be assigned to groups, and new members can be auto-added." }, + { question: "What are permission groups and how do they work?", answer: "Permission groups are an Enterprise access control feature that lets workspace admins define granular restrictions beyond the standard Read/Write/Admin roles. Groups are scoped to a single workspace: each user can be in at most one group per workspace, and a user can be in different groups across different workspaces. A permission group can hide UI sections (like trace spans, knowledge base, API keys, or deployment options), disable features (MCP tools, custom tools, skills, invitations), and restrict which integrations and model providers its members can access. Members can be assigned manually, and new members can be auto-added on join. Execution-time enforcement is based on the workflow's workspace, not the user's current UI context." }, { question: "How should I set up permissions for a new team member?", answer: "Start with the lowest permission level they need. Invite them to the organization as a Member, then add them to the relevant workspace with Read permission if they only need visibility, Write if they need to create and run workflows, or Admin if they need to manage the workspace and its users. You can always increase permissions later." }, ]} /> \ No newline at end of file diff --git a/apps/docs/content/docs/en/skills/index.mdx b/apps/docs/content/docs/en/skills/index.mdx index 611841db370..73a01f9ad4f 100644 --- a/apps/docs/content/docs/en/skills/index.mdx +++ b/apps/docs/content/docs/en/skills/index.mdx @@ -140,7 +140,7 @@ import { FAQ } from '@/components/ui/faq' { question: "How does the agent decide when to load a skill?", answer: "The agent sees an available_skills section in its system prompt listing each skill's name and description. When the agent determines that a skill is relevant to the current task, it calls the load_skill tool with the skill name. The full skill content is then returned as a tool response. This is why writing a specific, keyword-rich description is critical -- it is the only thing the agent reads before deciding whether to activate a skill." }, { question: "Do skills work with all LLM providers?", answer: "Yes. The load_skill mechanism uses standard tool-calling, which is supported by all LLM providers in Sim. No provider-specific configuration is needed. The skill system works the same way whether you are using Anthropic, OpenAI, Google, or any other supported provider." }, { 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. Organization admins can create permission groups with the disableSkills option enabled. When a user is assigned to such a permission group, the skills dropdown in agent blocks will be disabled and they will not be able to add or use skills in their workflows." }, + { 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: "Go to Settings and select Skills under the Tools section. From there you can add new skills with a name (kebab-case identifier, max 64 characters), description (max 1024 characters), and content (full instructions in markdown). You can also edit or delete existing skills from this page." }, ]} /> diff --git a/apps/sim/app/api/guardrails/validate/route.ts b/apps/sim/app/api/guardrails/validate/route.ts index 02f4e4f1945..157ed3f6907 100644 --- a/apps/sim/app/api/guardrails/validate/route.ts +++ b/apps/sim/app/api/guardrails/validate/route.ts @@ -7,6 +7,11 @@ import { validateHallucination } from '@/lib/guardrails/validate_hallucination' import { validateJson } from '@/lib/guardrails/validate_json' import { validatePII } from '@/lib/guardrails/validate_pii' import { validateRegex } from '@/lib/guardrails/validate_regex' +import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils' +import { + assertPermissionsAllowed, + ProviderNotAllowedError, +} from '@/ee/access-control/utils/permission-check' const logger = createLogger('GuardrailsValidateAPI') @@ -39,7 +44,6 @@ export const POST = withRouteHandler(async (request: NextRequest) => { bedrockSecretKey, bedrockRegion, workflowId, - workspaceId, piiEntityTypes, piiMode, piiLanguage, @@ -110,6 +114,64 @@ export const POST = withRouteHandler(async (request: NextRequest) => { }) } + let resolvedWorkspaceId: string | undefined + + if (validationType === 'hallucination' && model) { + if (!workflowId || typeof workflowId !== 'string') { + return NextResponse.json({ + success: true, + output: { + passed: false, + validationType, + input: input || '', + error: + 'Workflow context is required for hallucination validation. Call this endpoint via a workflow execution, not directly.', + }, + }) + } + + const authorization = await authorizeWorkflowByWorkspacePermission({ + workflowId, + userId: auth.userId, + action: 'read', + }) + + if (!authorization.allowed || !authorization.workflow?.workspaceId) { + return NextResponse.json({ + success: true, + output: { + passed: false, + validationType, + input: input || '', + error: authorization.message || 'Workflow not found or access denied.', + }, + }) + } + + resolvedWorkspaceId = authorization.workflow.workspaceId + + try { + await assertPermissionsAllowed({ + userId: auth.userId, + workspaceId: resolvedWorkspaceId, + model, + }) + } catch (err) { + if (err instanceof ProviderNotAllowedError) { + return NextResponse.json({ + success: true, + output: { + passed: false, + validationType, + input: input || '', + error: err.message, + }, + }) + } + throw err + } + } + const inputStr = convertInputToString(input) logger.info(`[${requestId}] Executing validation locally`, { @@ -141,7 +203,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { bedrockRegion, }, workflowId, - workspaceId, + resolvedWorkspaceId, piiEntityTypes, piiMode, piiLanguage, diff --git a/apps/sim/app/api/mcp/tools/execute/route.ts b/apps/sim/app/api/mcp/tools/execute/route.ts index 989ea5b544f..c94dc7d0aba 100644 --- a/apps/sim/app/api/mcp/tools/execute/route.ts +++ b/apps/sim/app/api/mcp/tools/execute/route.ts @@ -14,6 +14,10 @@ import { createMcpSuccessResponse, validateStringParam, } from '@/lib/mcp/utils' +import { + assertPermissionsAllowed, + McpToolsNotAllowedError, +} from '@/ee/access-control/utils/permission-check' const logger = createLogger('McpToolExecutionAPI') @@ -72,6 +76,19 @@ export const POST = withRouteHandler( return createMcpErrorResponse(new Error(toolNameValidation.error), 'Invalid toolName', 400) } + try { + await assertPermissionsAllowed({ + userId, + workspaceId, + toolKind: 'mcp', + }) + } catch (err) { + if (err instanceof McpToolsNotAllowedError) { + return createMcpErrorResponse(err, err.message, 403) + } + throw err + } + logger.info( `[${requestId}] Executing tool ${toolName} on server ${serverId} for user ${userId} in workspace ${workspaceId}` ) diff --git a/apps/sim/app/api/mothership/execute/route.ts b/apps/sim/app/api/mothership/execute/route.ts index 23f0111c962..31fd259fbf2 100644 --- a/apps/sim/app/api/mothership/execute/route.ts +++ b/apps/sim/app/api/mothership/execute/route.ts @@ -82,7 +82,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => { }) const [workspaceContext, integrationTools, userPermission] = await Promise.all([ generateWorkspaceContext(workspaceId, userId), - buildIntegrationToolSchemas(userId, messageId), + buildIntegrationToolSchemas(userId, messageId, undefined, workspaceId), getUserEntityPermissions(userId, 'workspace', workspaceId).catch(() => null), ]) diff --git a/apps/sim/app/api/organizations/[id]/invitations/route.ts b/apps/sim/app/api/organizations/[id]/invitations/route.ts index 1c6912bcea5..68281a96b4e 100644 --- a/apps/sim/app/api/organizations/[id]/invitations/route.ts +++ b/apps/sim/app/api/organizations/[id]/invitations/route.ts @@ -94,9 +94,10 @@ export const POST = withRouteHandler( return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } - await validateInvitationsAllowed(session.user.id) - const { id: organizationId } = await params + + await validateInvitationsAllowed(session.user.id, { organizationId }) + const url = new URL(request.url) const validateOnly = url.searchParams.get('validate') === 'true' const isBatch = url.searchParams.get('batch') === 'true' @@ -213,6 +214,8 @@ export const POST = withRouteHandler( ) } + await validateInvitationsAllowed(session.user.id, wsInvitation.workspaceId) + validGrants.push({ workspaceId: wsInvitation.workspaceId, permission: wsInvitation.permission, diff --git a/apps/sim/app/api/organizations/[id]/members/route.ts b/apps/sim/app/api/organizations/[id]/members/route.ts index 0b6459a92fc..b810226fd53 100644 --- a/apps/sim/app/api/organizations/[id]/members/route.ts +++ b/apps/sim/app/api/organizations/[id]/members/route.ts @@ -165,9 +165,10 @@ export const POST = withRouteHandler( return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } - await validateInvitationsAllowed(session.user.id) - const { id: organizationId } = await params + + await validateInvitationsAllowed(session.user.id, { organizationId }) + const { email, role = 'member' } = await request.json() if (!email) { diff --git a/apps/sim/app/api/permission-groups/[id]/members/bulk/route.ts b/apps/sim/app/api/permission-groups/[id]/members/bulk/route.ts deleted file mode 100644 index 7d8c9eba15c..00000000000 --- a/apps/sim/app/api/permission-groups/[id]/members/bulk/route.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { db } from '@sim/db' -import { member, permissionGroup, permissionGroupMember } from '@sim/db/schema' -import { createLogger } from '@sim/logger' -import { generateId } from '@sim/utils/id' -import { and, eq, inArray } from 'drizzle-orm' -import { type NextRequest, NextResponse } from 'next/server' -import { z } from 'zod' -import { getSession } from '@/lib/auth' -import { hasAccessControlAccess } from '@/lib/billing' -import { withRouteHandler } from '@/lib/core/utils/with-route-handler' - -const logger = createLogger('PermissionGroupBulkMembers') - -async function getPermissionGroupWithAccess(groupId: string, userId: string) { - const [group] = await db - .select({ - id: permissionGroup.id, - organizationId: permissionGroup.organizationId, - }) - .from(permissionGroup) - .where(eq(permissionGroup.id, groupId)) - .limit(1) - - if (!group) return null - - const [membership] = await db - .select({ role: member.role }) - .from(member) - .where(and(eq(member.userId, userId), eq(member.organizationId, group.organizationId))) - .limit(1) - - if (!membership) return null - - return { group, role: membership.role } -} - -const bulkAddSchema = z.object({ - userIds: z.array(z.string()).optional(), - addAllOrgMembers: z.boolean().optional(), -}) - -export const POST = withRouteHandler( - async (req: NextRequest, { params }: { params: Promise<{ id: string }> }) => { - const session = await getSession() - - if (!session?.user?.id) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) - } - - const { id } = await params - - try { - const hasAccess = await hasAccessControlAccess(session.user.id) - if (!hasAccess) { - return NextResponse.json( - { error: 'Access Control is an Enterprise feature' }, - { status: 403 } - ) - } - - const result = await getPermissionGroupWithAccess(id, session.user.id) - - if (!result) { - return NextResponse.json({ error: 'Permission group not found' }, { status: 404 }) - } - - if (result.role !== 'admin' && result.role !== 'owner') { - return NextResponse.json({ error: 'Admin or owner permissions required' }, { status: 403 }) - } - - const body = await req.json() - const { userIds, addAllOrgMembers } = bulkAddSchema.parse(body) - - let targetUserIds: string[] = [] - - if (addAllOrgMembers) { - const orgMembers = await db - .select({ userId: member.userId }) - .from(member) - .where(eq(member.organizationId, result.group.organizationId)) - - targetUserIds = orgMembers.map((m) => m.userId) - } else if (userIds && userIds.length > 0) { - const validMembers = await db - .select({ userId: member.userId }) - .from(member) - .where( - and( - eq(member.organizationId, result.group.organizationId), - inArray(member.userId, userIds) - ) - ) - - targetUserIds = validMembers.map((m) => m.userId) - } - - if (targetUserIds.length === 0) { - return NextResponse.json({ added: 0, moved: 0 }) - } - - const existingMemberships = await db - .select({ - id: permissionGroupMember.id, - userId: permissionGroupMember.userId, - permissionGroupId: permissionGroupMember.permissionGroupId, - }) - .from(permissionGroupMember) - .where(inArray(permissionGroupMember.userId, targetUserIds)) - - const alreadyInThisGroup = new Set( - existingMemberships.filter((m) => m.permissionGroupId === id).map((m) => m.userId) - ) - const usersToAdd = targetUserIds.filter((uid) => !alreadyInThisGroup.has(uid)) - - if (usersToAdd.length === 0) { - return NextResponse.json({ added: 0, moved: 0 }) - } - - const membershipsToDelete = existingMemberships.filter( - (m) => m.permissionGroupId !== id && usersToAdd.includes(m.userId) - ) - const movedCount = membershipsToDelete.length - - await db.transaction(async (tx) => { - if (membershipsToDelete.length > 0) { - await tx.delete(permissionGroupMember).where( - inArray( - permissionGroupMember.id, - membershipsToDelete.map((m) => m.id) - ) - ) - } - - const newMembers = usersToAdd.map((userId) => ({ - id: generateId(), - permissionGroupId: id, - userId, - assignedBy: session.user.id, - assignedAt: new Date(), - })) - - await tx.insert(permissionGroupMember).values(newMembers) - }) - - logger.info('Bulk added members to permission group', { - permissionGroupId: id, - addedCount: usersToAdd.length, - movedCount, - assignedBy: session.user.id, - }) - - return NextResponse.json({ added: usersToAdd.length, moved: movedCount }) - } catch (error) { - if (error instanceof z.ZodError) { - return NextResponse.json({ error: error.errors[0].message }, { status: 400 }) - } - if ( - error instanceof Error && - error.message.includes('permission_group_member_user_id_unique') - ) { - return NextResponse.json( - { error: 'One or more users are already in a permission group' }, - { status: 409 } - ) - } - logger.error('Error bulk adding members to permission group', error) - return NextResponse.json({ error: 'Failed to add members' }, { status: 500 }) - } - } -) diff --git a/apps/sim/app/api/permission-groups/route.ts b/apps/sim/app/api/permission-groups/route.ts deleted file mode 100644 index 69157fd9afd..00000000000 --- a/apps/sim/app/api/permission-groups/route.ts +++ /dev/null @@ -1,231 +0,0 @@ -import { db } from '@sim/db' -import { member, organization, permissionGroup, permissionGroupMember, user } from '@sim/db/schema' -import { createLogger } from '@sim/logger' -import { generateId } from '@sim/utils/id' -import { and, count, desc, eq } from 'drizzle-orm' -import { NextResponse } from 'next/server' -import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' -import { getSession } from '@/lib/auth' -import { hasAccessControlAccess } from '@/lib/billing' -import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { - DEFAULT_PERMISSION_GROUP_CONFIG, - type PermissionGroupConfig, - parsePermissionGroupConfig, -} from '@/lib/permission-groups/types' - -const logger = createLogger('PermissionGroups') - -const configSchema = z.object({ - allowedIntegrations: z.array(z.string()).nullable().optional(), - allowedModelProviders: z.array(z.string()).nullable().optional(), - hideTraceSpans: z.boolean().optional(), - hideKnowledgeBaseTab: z.boolean().optional(), - hideTablesTab: z.boolean().optional(), - hideCopilot: z.boolean().optional(), - hideIntegrationsTab: z.boolean().optional(), - hideSecretsTab: z.boolean().optional(), - hideApiKeysTab: z.boolean().optional(), - hideInboxTab: z.boolean().optional(), - hideEnvironmentTab: z.boolean().optional(), - hideFilesTab: z.boolean().optional(), - disableMcpTools: z.boolean().optional(), - disableCustomTools: z.boolean().optional(), - disableSkills: z.boolean().optional(), - hideTemplates: z.boolean().optional(), - disableInvitations: z.boolean().optional(), - disablePublicApi: z.boolean().optional(), - hideDeployApi: z.boolean().optional(), - hideDeployMcp: z.boolean().optional(), - hideDeployA2a: z.boolean().optional(), - hideDeployChatbot: z.boolean().optional(), - hideDeployTemplate: z.boolean().optional(), -}) - -const createSchema = z.object({ - organizationId: z.string().min(1), - name: z.string().trim().min(1).max(100), - description: z.string().max(500).optional(), - config: configSchema.optional(), - autoAddNewMembers: z.boolean().optional(), -}) - -export const GET = withRouteHandler(async (req: Request) => { - const session = await getSession() - - if (!session?.user?.id) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) - } - - const { searchParams } = new URL(req.url) - const organizationId = searchParams.get('organizationId') - - if (!organizationId) { - return NextResponse.json({ error: 'organizationId is required' }, { status: 400 }) - } - - const membership = await db - .select({ id: member.id, role: member.role }) - .from(member) - .where(and(eq(member.userId, session.user.id), eq(member.organizationId, organizationId))) - .limit(1) - - if (membership.length === 0) { - return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) - } - - const groups = await db - .select({ - id: permissionGroup.id, - name: permissionGroup.name, - description: permissionGroup.description, - config: permissionGroup.config, - createdBy: permissionGroup.createdBy, - createdAt: permissionGroup.createdAt, - updatedAt: permissionGroup.updatedAt, - autoAddNewMembers: permissionGroup.autoAddNewMembers, - creatorName: user.name, - creatorEmail: user.email, - }) - .from(permissionGroup) - .leftJoin(user, eq(permissionGroup.createdBy, user.id)) - .where(eq(permissionGroup.organizationId, organizationId)) - .orderBy(desc(permissionGroup.createdAt)) - - const groupsWithCounts = await Promise.all( - groups.map(async (group) => { - const [memberCount] = await db - .select({ count: count() }) - .from(permissionGroupMember) - .where(eq(permissionGroupMember.permissionGroupId, group.id)) - - return { - ...group, - config: parsePermissionGroupConfig(group.config), - memberCount: memberCount?.count ?? 0, - } - }) - ) - - return NextResponse.json({ permissionGroups: groupsWithCounts }) -}) - -export const POST = withRouteHandler(async (req: Request) => { - const session = await getSession() - - if (!session?.user?.id) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) - } - - try { - const hasAccess = await hasAccessControlAccess(session.user.id) - if (!hasAccess) { - return NextResponse.json( - { error: 'Access Control is an Enterprise feature' }, - { status: 403 } - ) - } - - const body = await req.json() - const { organizationId, name, description, config, autoAddNewMembers } = - createSchema.parse(body) - - const membership = await db - .select({ id: member.id, role: member.role }) - .from(member) - .where(and(eq(member.userId, session.user.id), eq(member.organizationId, organizationId))) - .limit(1) - - const role = membership[0]?.role - if (membership.length === 0 || (role !== 'admin' && role !== 'owner')) { - return NextResponse.json({ error: 'Admin or owner permissions required' }, { status: 403 }) - } - - const orgExists = await db - .select({ id: organization.id }) - .from(organization) - .where(eq(organization.id, organizationId)) - .limit(1) - - if (orgExists.length === 0) { - return NextResponse.json({ error: 'Organization not found' }, { status: 404 }) - } - - const existingGroup = await db - .select({ id: permissionGroup.id }) - .from(permissionGroup) - .where( - and(eq(permissionGroup.organizationId, organizationId), eq(permissionGroup.name, name)) - ) - .limit(1) - - if (existingGroup.length > 0) { - return NextResponse.json( - { error: 'A permission group with this name already exists' }, - { status: 409 } - ) - } - - const groupConfig: PermissionGroupConfig = { - ...DEFAULT_PERMISSION_GROUP_CONFIG, - ...config, - } - - const now = new Date() - const newGroup = { - id: generateId(), - organizationId, - name, - description: description || null, - config: groupConfig, - createdBy: session.user.id, - createdAt: now, - updatedAt: now, - autoAddNewMembers: autoAddNewMembers || false, - } - - await db.transaction(async (tx) => { - if (autoAddNewMembers) { - await tx - .update(permissionGroup) - .set({ autoAddNewMembers: false, updatedAt: now }) - .where( - and( - eq(permissionGroup.organizationId, organizationId), - eq(permissionGroup.autoAddNewMembers, true) - ) - ) - } - await tx.insert(permissionGroup).values(newGroup) - }) - - logger.info('Created permission group', { - permissionGroupId: newGroup.id, - organizationId, - userId: session.user.id, - }) - - recordAudit({ - workspaceId: null, - actorId: session.user.id, - action: AuditAction.PERMISSION_GROUP_CREATED, - resourceType: AuditResourceType.PERMISSION_GROUP, - resourceId: newGroup.id, - actorName: session.user.name ?? undefined, - actorEmail: session.user.email ?? undefined, - resourceName: name, - description: `Created permission group "${name}"`, - metadata: { organizationId, autoAddNewMembers: autoAddNewMembers || false }, - request: req, - }) - - return NextResponse.json({ permissionGroup: newGroup }, { status: 201 }) - } catch (error) { - if (error instanceof z.ZodError) { - return NextResponse.json({ error: error.errors[0].message }, { status: 400 }) - } - logger.error('Error creating permission group', error) - return NextResponse.json({ error: 'Failed to create permission group' }, { status: 500 }) - } -}) diff --git a/apps/sim/app/api/permission-groups/user/route.ts b/apps/sim/app/api/permission-groups/user/route.ts index d4751cd7ace..4dbddafb599 100644 --- a/apps/sim/app/api/permission-groups/user/route.ts +++ b/apps/sim/app/api/permission-groups/user/route.ts @@ -1,43 +1,41 @@ import { db } from '@sim/db' -import { member, permissionGroup, permissionGroupMember } from '@sim/db/schema' -import { and, eq } from 'drizzle-orm' +import { permissionGroup, permissionGroupMember } from '@sim/db/schema' +import { and, asc, eq } from 'drizzle-orm' import { NextResponse } from 'next/server' import { getSession } from '@/lib/auth' -import { isOrganizationOnEnterprisePlan } from '@/lib/billing' +import { isWorkspaceOnEnterprisePlan } from '@/lib/billing' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { parsePermissionGroupConfig } from '@/lib/permission-groups/types' +import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils' export const GET = withRouteHandler(async (req: Request) => { const session = await getSession() - if (!session?.user?.id) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } const { searchParams } = new URL(req.url) - const organizationId = searchParams.get('organizationId') + const workspaceId = searchParams.get('workspaceId') - if (!organizationId) { - return NextResponse.json({ error: 'organizationId is required' }, { status: 400 }) + if (!workspaceId) { + return NextResponse.json({ error: 'workspaceId is required' }, { status: 400 }) } - const [membership] = await db - .select({ id: member.id }) - .from(member) - .where(and(eq(member.userId, session.user.id), eq(member.organizationId, organizationId))) - .limit(1) - - if (!membership) { - return NextResponse.json({ error: 'Not a member of this organization' }, { status: 403 }) + const access = await checkWorkspaceAccess(workspaceId, session.user.id) + if (!access.exists) { + return NextResponse.json({ error: 'Workspace not found' }, { status: 404 }) + } + if (!access.hasAccess) { + return NextResponse.json({ error: 'Not a member of this workspace' }, { status: 403 }) } - // Short-circuit: if org is not on enterprise plan, ignore permission configs - const isEnterprise = await isOrganizationOnEnterprisePlan(organizationId) + const isEnterprise = await isWorkspaceOnEnterprisePlan(workspaceId) if (!isEnterprise) { return NextResponse.json({ permissionGroupId: null, groupName: null, config: null, + entitled: false, }) } @@ -52,9 +50,10 @@ export const GET = withRouteHandler(async (req: Request) => { .where( and( eq(permissionGroupMember.userId, session.user.id), - eq(permissionGroup.organizationId, organizationId) + eq(permissionGroup.workspaceId, workspaceId) ) ) + .orderBy(asc(permissionGroup.createdAt), asc(permissionGroup.id)) .limit(1) if (!groupMembership) { @@ -62,6 +61,7 @@ export const GET = withRouteHandler(async (req: Request) => { permissionGroupId: null, groupName: null, config: null, + entitled: true, }) } @@ -69,5 +69,6 @@ export const GET = withRouteHandler(async (req: Request) => { permissionGroupId: groupMembership.permissionGroupId, groupName: groupMembership.groupName, config: parsePermissionGroupConfig(groupMembership.config), + entitled: true, }) }) diff --git a/apps/sim/app/api/providers/route.ts b/apps/sim/app/api/providers/route.ts index 06e6a4c28ad..ddf2f0d59c0 100644 --- a/apps/sim/app/api/providers/route.ts +++ b/apps/sim/app/api/providers/route.ts @@ -13,6 +13,11 @@ import { refreshTokenIfNeeded, resolveOAuthAccountId, } from '@/app/api/auth/oauth/utils' +import { + assertPermissionsAllowed, + IntegrationNotAllowedError, + ProviderNotAllowedError, +} from '@/ee/access-control/utils/permission-check' import type { StreamingExecution } from '@/executor/types' import { executeProviderRequest } from '@/providers' @@ -103,6 +108,19 @@ export const POST = withRouteHandler(async (request: NextRequest) => { if (!workspaceAccess.hasAccess) { return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) } + + try { + await assertPermissionsAllowed({ + userId: auth.userId, + workspaceId, + model, + }) + } catch (err) { + if (err instanceof ProviderNotAllowedError || err instanceof IntegrationNotAllowedError) { + return NextResponse.json({ error: err.message }, { status: 403 }) + } + throw err + } } let finalApiKey: string | undefined = apiKey diff --git a/apps/sim/app/api/v1/admin/access-control/route.ts b/apps/sim/app/api/v1/admin/access-control/route.ts index e0f18bd2094..d1cd6f9253a 100644 --- a/apps/sim/app/api/v1/admin/access-control/route.ts +++ b/apps/sim/app/api/v1/admin/access-control/route.ts @@ -5,24 +5,28 @@ * List all permission groups with optional filtering. * * Query Parameters: - * - organizationId?: string - Filter by organization ID + * - workspaceId?: string - Filter by workspace ID + * - organizationId?: string - Filter by organization ID (joins via workspace) * * Response: { data: AdminPermissionGroup[], pagination: PaginationMeta } * * DELETE /api/v1/admin/access-control - * Delete permission groups for an organization. + * Delete permission groups scoped to a workspace or organization (via workspace join). * Used when an enterprise plan churns to clean up access control data. * * Query Parameters: - * - organizationId: string - Delete all permission groups for this organization + * - workspaceId?: string - Delete all permission groups for this workspace + * - organizationId?: string - Delete all permission groups for every workspace in this org + * - reason?: string - Reason recorded in audit log (default: "Enterprise plan churn cleanup") * * Response: { success: true, deletedCount: number, membersRemoved: number } */ import { db } from '@sim/db' -import { organization, permissionGroup, permissionGroupMember, user } from '@sim/db/schema' +import { permissionGroup, permissionGroupMember, user, workspace } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { count, eq, inArray, sql } from 'drizzle-orm' +import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { withAdminAuth } from '@/app/api/v1/admin/middleware' import { @@ -35,8 +39,9 @@ const logger = createLogger('AdminAccessControlAPI') export interface AdminPermissionGroup { id: string - organizationId: string - organizationName: string | null + workspaceId: string + workspaceName: string | null + organizationId: string | null name: string description: string | null memberCount: number @@ -48,14 +53,16 @@ export interface AdminPermissionGroup { export const GET = withRouteHandler( withAdminAuth(async (request) => { const url = new URL(request.url) + const workspaceId = url.searchParams.get('workspaceId') const organizationId = url.searchParams.get('organizationId') try { const baseQuery = db .select({ id: permissionGroup.id, - organizationId: permissionGroup.organizationId, - organizationName: organization.name, + workspaceId: permissionGroup.workspaceId, + workspaceName: workspace.name, + workspaceOrganizationId: workspace.organizationId, name: permissionGroup.name, description: permissionGroup.description, createdAt: permissionGroup.createdAt, @@ -63,12 +70,14 @@ export const GET = withRouteHandler( createdByEmail: user.email, }) .from(permissionGroup) - .leftJoin(organization, eq(permissionGroup.organizationId, organization.id)) + .leftJoin(workspace, eq(permissionGroup.workspaceId, workspace.id)) .leftJoin(user, eq(permissionGroup.createdBy, user.id)) let groups - if (organizationId) { - groups = await baseQuery.where(eq(permissionGroup.organizationId, organizationId)) + if (workspaceId) { + groups = await baseQuery.where(eq(permissionGroup.workspaceId, workspaceId)) + } else if (organizationId) { + groups = await baseQuery.where(eq(workspace.organizationId, organizationId)) } else { groups = await baseQuery } @@ -82,8 +91,9 @@ export const GET = withRouteHandler( return { id: group.id, - organizationId: group.organizationId, - organizationName: group.organizationName, + workspaceId: group.workspaceId, + workspaceName: group.workspaceName, + organizationId: group.workspaceOrganizationId, name: group.name, description: group.description, memberCount: memberCount?.count ?? 0, @@ -95,6 +105,7 @@ export const GET = withRouteHandler( ) logger.info('Admin API: Listed permission groups', { + workspaceId, organizationId, count: groupsWithCounts.length, }) @@ -109,7 +120,11 @@ export const GET = withRouteHandler( }, }) } catch (error) { - logger.error('Admin API: Failed to list permission groups', { error, organizationId }) + logger.error('Admin API: Failed to list permission groups', { + error, + workspaceId, + organizationId, + }) return internalErrorResponse('Failed to list permission groups') } }) @@ -118,26 +133,36 @@ export const GET = withRouteHandler( export const DELETE = withRouteHandler( withAdminAuth(async (request) => { const url = new URL(request.url) + const workspaceId = url.searchParams.get('workspaceId') const organizationId = url.searchParams.get('organizationId') const reason = url.searchParams.get('reason') || 'Enterprise plan churn cleanup' - if (!organizationId) { - return badRequestResponse('organizationId is required') + if (!workspaceId && !organizationId) { + return badRequestResponse('workspaceId or organizationId is required') } try { - const existingGroups = await db - .select({ id: permissionGroup.id }) + const selectBase = db + .select({ + id: permissionGroup.id, + workspaceId: permissionGroup.workspaceId, + name: permissionGroup.name, + }) .from(permissionGroup) - .where(eq(permissionGroup.organizationId, organizationId)) + + const existingGroups = workspaceId + ? await selectBase.where(eq(permissionGroup.workspaceId, workspaceId)) + : await selectBase + .innerJoin(workspace, eq(workspace.id, permissionGroup.workspaceId)) + .where(eq(workspace.organizationId, organizationId!)) if (existingGroups.length === 0) { - logger.info('Admin API: No permission groups to delete', { organizationId }) + logger.info('Admin API: No permission groups to delete', { workspaceId, organizationId }) return singleResponse({ success: true, deletedCount: 0, membersRemoved: 0, - message: 'No permission groups found for the given organization', + message: 'No permission groups found for the given scope', }) } @@ -150,10 +175,24 @@ export const DELETE = withRouteHandler( const membersToRemove = Number(memberCountResult?.count ?? 0) - // Members are deleted via cascade when permission groups are deleted - await db.delete(permissionGroup).where(eq(permissionGroup.organizationId, organizationId)) + await db.delete(permissionGroup).where(inArray(permissionGroup.id, groupIds)) + + for (const group of existingGroups) { + recordAudit({ + workspaceId: group.workspaceId, + actorId: 'admin-api', + action: AuditAction.PERMISSION_GROUP_DELETED, + resourceType: AuditResourceType.PERMISSION_GROUP, + resourceId: group.id, + resourceName: group.name, + description: `Admin API deleted permission group "${group.name}"`, + metadata: { reason, workspaceId: group.workspaceId, organizationId }, + request, + }) + } logger.info('Admin API: Deleted permission groups', { + workspaceId, organizationId, deletedCount: existingGroups.length, membersRemoved: membersToRemove, @@ -167,7 +206,11 @@ export const DELETE = withRouteHandler( reason, }) } catch (error) { - logger.error('Admin API: Failed to delete permission groups', { error, organizationId }) + logger.error('Admin API: Failed to delete permission groups', { + error, + workspaceId, + organizationId, + }) return internalErrorResponse('Failed to delete permission groups') } }) 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 7d243e5bab6..8e03e3c491b 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 @@ -37,6 +37,7 @@ import { generateId } from '@sim/utils/id' import { and, count, eq } from 'drizzle-orm' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { syncWorkspaceEnvCredentials } from '@/lib/credentials/environment' +import { applyWorkspaceAutoAddGroup } from '@/lib/permission-groups/auto-add' import { getWorkspaceById } from '@/lib/workspaces/permissions/utils' import { withAdminAuthParams } from '@/app/api/v1/admin/middleware' import { @@ -229,6 +230,8 @@ export const POST = withRouteHandler( updatedAt: now, }) + await applyWorkspaceAutoAddGroup(db, workspaceId, body.userId) + logger.info(`Admin API: Added user ${body.userId} to workspace ${workspaceId}`, { permissions: body.permissions, permissionId, diff --git a/apps/sim/app/api/workflows/[id]/deploy/route.ts b/apps/sim/app/api/workflows/[id]/deploy/route.ts index 64172ab8cc3..dd57844def7 100644 --- a/apps/sim/app/api/workflows/[id]/deploy/route.ts +++ b/apps/sim/app/api/workflows/[id]/deploy/route.ts @@ -12,6 +12,10 @@ import { createErrorResponse, createSuccessResponse, } from '@/app/api/workflows/utils' +import { + PublicApiNotAllowedError, + validatePublicApiAllowed, +} from '@/ee/access-control/utils/permission-check' const logger = createLogger('WorkflowDeployAPI') @@ -154,11 +158,8 @@ export const PATCH = withRouteHandler( } if (isPublicApi) { - const { validatePublicApiAllowed, PublicApiNotAllowedError } = await import( - '@/ee/access-control/utils/permission-check' - ) try { - await validatePublicApiAllowed(session?.user?.id) + await validatePublicApiAllowed(session?.user?.id, workflowData?.workspaceId ?? undefined) } catch (err) { if (err instanceof PublicApiNotAllowedError) { return createErrorResponse('Public API access is disabled', 403) diff --git a/apps/sim/app/api/workflows/[id]/execute/route.ts b/apps/sim/app/api/workflows/[id]/execute/route.ts index 723ca296054..9df86fbf771 100644 --- a/apps/sim/app/api/workflows/[id]/execute/route.ts +++ b/apps/sim/app/api/workflows/[id]/execute/route.ts @@ -1,6 +1,9 @@ +import { db } from '@sim/db' +import { workflow as workflowTable } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { generateId, isValidUuid } from '@sim/utils/id' +import { eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { AuthType, checkHybridAuth, hasExternalApiCredentials } from '@/lib/auth/hybrid' @@ -48,6 +51,10 @@ import { workflowHasResponseBlock, } from '@/lib/workflows/utils' import { executeWorkflowJob, type WorkflowExecutionPayload } from '@/background/workflow-execution' +import { + PublicApiNotAllowedError, + validatePublicApiAllowed, +} from '@/ee/access-control/utils/permission-check' import { normalizeName } from '@/executor/constants' import { ExecutionSnapshot } from '@/executor/execution/snapshot' import type { @@ -312,31 +319,28 @@ async function handleExecutePost( return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) } - const { db: dbClient, workflow: workflowTable } = await import('@sim/db') - const { eq } = await import('drizzle-orm') - const [wf] = await dbClient + const [wf] = await db .select({ isPublicApi: workflowTable.isPublicApi, isDeployed: workflowTable.isDeployed, userId: workflowTable.userId, + workspaceId: workflowTable.workspaceId, }) .from(workflowTable) .where(eq(workflowTable.id, workflowId)) .limit(1) - if (!wf?.isPublicApi || !wf.isDeployed) { - return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) - } - - const { isPublicApiDisabled } = await import('@/lib/core/config/feature-flags') - if (isPublicApiDisabled) { + if (!wf?.isPublicApi || !wf.isDeployed || !wf.workspaceId) { return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) } - const { getUserPermissionConfig } = await import('@/ee/access-control/utils/permission-check') - const ownerConfig = await getUserPermissionConfig(wf.userId) - if (ownerConfig?.disablePublicApi) { - return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + try { + await validatePublicApiAllowed(wf.userId, wf.workspaceId) + } catch (err) { + if (err instanceof PublicApiNotAllowedError) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + throw err } userId = wf.userId diff --git a/apps/sim/app/api/workspaces/[id]/permission-groups/[groupId]/members/bulk/route.ts b/apps/sim/app/api/workspaces/[id]/permission-groups/[groupId]/members/bulk/route.ts new file mode 100644 index 00000000000..66a60ec2cee --- /dev/null +++ b/apps/sim/app/api/workspaces/[id]/permission-groups/[groupId]/members/bulk/route.ts @@ -0,0 +1,208 @@ +import { db } from '@sim/db' +import { permissionGroup, permissionGroupMember, permissions } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { getPostgresConstraintName, getPostgresErrorCode } from '@sim/utils/errors' +import { generateId } from '@sim/utils/id' +import { and, eq, inArray } from 'drizzle-orm' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' +import { getSession } from '@/lib/auth' +import { isWorkspaceOnEnterprisePlan } from '@/lib/billing' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { PERMISSION_GROUP_MEMBER_CONSTRAINTS } from '@/lib/permission-groups/types' +import { hasWorkspaceAdminAccess } from '@/lib/workspaces/permissions/utils' + +const logger = createLogger('WorkspacePermissionGroupBulkMembers') + +async function loadGroupInWorkspace(groupId: string, workspaceId: string) { + const [group] = await db + .select({ + id: permissionGroup.id, + workspaceId: permissionGroup.workspaceId, + name: permissionGroup.name, + }) + .from(permissionGroup) + .where(and(eq(permissionGroup.id, groupId), eq(permissionGroup.workspaceId, workspaceId))) + .limit(1) + + return group ?? null +} + +const bulkAddSchema = z.object({ + userIds: z.array(z.string()).optional(), + addAllWorkspaceMembers: z.boolean().optional(), +}) + +export const POST = withRouteHandler( + async (req: NextRequest, { params }: { params: Promise<{ id: string; groupId: string }> }) => { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const { id: workspaceId, groupId: id } = await params + + try { + const isWorkspaceAdmin = await hasWorkspaceAdminAccess(session.user.id, workspaceId) + if (!isWorkspaceAdmin) { + return NextResponse.json({ error: 'Admin permissions required' }, { status: 403 }) + } + + const entitled = await isWorkspaceOnEnterprisePlan(workspaceId) + if (!entitled) { + return NextResponse.json( + { error: 'Access Control is an Enterprise feature' }, + { status: 403 } + ) + } + + const group = await loadGroupInWorkspace(id, workspaceId) + if (!group) { + return NextResponse.json({ error: 'Permission group not found' }, { status: 404 }) + } + + const body = await req.json() + const { userIds, addAllWorkspaceMembers } = bulkAddSchema.parse(body) + + let targetUserIds: string[] = [] + + if (addAllWorkspaceMembers) { + const workspaceMembers = await db + .select({ userId: permissions.userId }) + .from(permissions) + .where( + and(eq(permissions.entityType, 'workspace'), eq(permissions.entityId, workspaceId)) + ) + + targetUserIds = Array.from(new Set(workspaceMembers.map((m) => m.userId))) + } else if (userIds && userIds.length > 0) { + const uniqueUserIds = Array.from(new Set(userIds)) + const validMembers = await db + .select({ userId: permissions.userId }) + .from(permissions) + .where( + and( + eq(permissions.entityType, 'workspace'), + eq(permissions.entityId, workspaceId), + inArray(permissions.userId, uniqueUserIds) + ) + ) + + targetUserIds = Array.from(new Set(validMembers.map((m) => m.userId))) + } + + if (targetUserIds.length === 0) { + return NextResponse.json({ added: 0, moved: 0 }) + } + + const { addedUserIds, movedCount } = await db.transaction(async (tx) => { + const existingMemberships = await tx + .select({ + id: permissionGroupMember.id, + userId: permissionGroupMember.userId, + permissionGroupId: permissionGroupMember.permissionGroupId, + }) + .from(permissionGroupMember) + .innerJoin( + permissionGroup, + eq(permissionGroupMember.permissionGroupId, permissionGroup.id) + ) + .where( + and( + eq(permissionGroup.workspaceId, workspaceId), + inArray(permissionGroupMember.userId, targetUserIds) + ) + ) + + const alreadyInThisGroup = new Set( + existingMemberships.filter((m) => m.permissionGroupId === id).map((m) => m.userId) + ) + const usersToAdd = targetUserIds.filter((uid) => !alreadyInThisGroup.has(uid)) + + if (usersToAdd.length === 0) { + return { addedUserIds: [] as string[], movedCount: 0 } + } + + const membershipsToDelete = existingMemberships.filter( + (m) => m.permissionGroupId !== id && usersToAdd.includes(m.userId) + ) + + if (membershipsToDelete.length > 0) { + await tx.delete(permissionGroupMember).where( + inArray( + permissionGroupMember.id, + membershipsToDelete.map((m) => m.id) + ) + ) + } + + const newMembers = usersToAdd.map((userId) => ({ + id: generateId(), + permissionGroupId: id, + workspaceId, + userId, + assignedBy: session.user.id, + assignedAt: new Date(), + })) + + await tx.insert(permissionGroupMember).values(newMembers) + + return { addedUserIds: usersToAdd, movedCount: membershipsToDelete.length } + }) + + if (addedUserIds.length === 0) { + return NextResponse.json({ added: 0, moved: 0 }) + } + + logger.info('Bulk added members to permission group', { + permissionGroupId: id, + workspaceId, + addedCount: addedUserIds.length, + movedCount, + assignedBy: session.user.id, + }) + + recordAudit({ + workspaceId, + actorId: session.user.id, + action: AuditAction.PERMISSION_GROUP_MEMBER_ADDED, + resourceType: AuditResourceType.PERMISSION_GROUP, + resourceId: id, + resourceName: group.name, + actorName: session.user.name ?? undefined, + actorEmail: session.user.email ?? undefined, + description: `Bulk added ${addedUserIds.length} member(s) to permission group "${group.name}"`, + metadata: { + permissionGroupId: id, + addedUserIds, + movedCount, + }, + request: req, + }) + + return NextResponse.json({ added: addedUserIds.length, moved: movedCount }) + } catch (error) { + if (error instanceof z.ZodError) { + return NextResponse.json({ error: error.errors[0].message }, { status: 400 }) + } + if (getPostgresErrorCode(error) === '23505') { + const constraint = getPostgresConstraintName(error) + if ( + constraint === PERMISSION_GROUP_MEMBER_CONSTRAINTS.workspaceUser || + constraint === PERMISSION_GROUP_MEMBER_CONSTRAINTS.groupUser + ) { + return NextResponse.json( + { + error: + 'One or more users were concurrently added to a group in this workspace. Please refresh and try again.', + }, + { status: 409 } + ) + } + } + logger.error('Error bulk adding members to permission group', error) + return NextResponse.json({ error: 'Failed to add members' }, { status: 500 }) + } + } +) diff --git a/apps/sim/app/api/permission-groups/[id]/members/route.ts b/apps/sim/app/api/workspaces/[id]/permission-groups/[groupId]/members/route.ts similarity index 55% rename from apps/sim/app/api/permission-groups/[id]/members/route.ts rename to apps/sim/app/api/workspaces/[id]/permission-groups/[groupId]/members/route.ts index d1e38849bce..41579e1c975 100644 --- a/apps/sim/app/api/permission-groups/[id]/members/route.ts +++ b/apps/sim/app/api/workspaces/[id]/permission-groups/[groupId]/members/route.ts @@ -1,53 +1,61 @@ import { db } from '@sim/db' -import { member, permissionGroup, permissionGroupMember, user } from '@sim/db/schema' +import { permissionGroup, permissionGroupMember, permissions, user } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { getPostgresConstraintName, getPostgresErrorCode } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' -import { and, eq } from 'drizzle-orm' +import { and, eq, inArray } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' -import { hasAccessControlAccess } from '@/lib/billing' +import { isWorkspaceOnEnterprisePlan } from '@/lib/billing' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { PERMISSION_GROUP_MEMBER_CONSTRAINTS } from '@/lib/permission-groups/types' +import { checkWorkspaceAccess, hasWorkspaceAdminAccess } from '@/lib/workspaces/permissions/utils' -const logger = createLogger('PermissionGroupMembers') +const logger = createLogger('WorkspacePermissionGroupMembers') -async function getPermissionGroupWithAccess(groupId: string, userId: string) { +async function loadGroupInWorkspace(groupId: string, workspaceId: string) { const [group] = await db .select({ id: permissionGroup.id, name: permissionGroup.name, - organizationId: permissionGroup.organizationId, + workspaceId: permissionGroup.workspaceId, }) .from(permissionGroup) - .where(eq(permissionGroup.id, groupId)) + .where(and(eq(permissionGroup.id, groupId), eq(permissionGroup.workspaceId, workspaceId))) .limit(1) - if (!group) return null - - const [membership] = await db - .select({ role: member.role }) - .from(member) - .where(and(eq(member.userId, userId), eq(member.organizationId, group.organizationId))) - .limit(1) - - if (!membership) return null - - return { group, role: membership.role } + return group ?? null } export const GET = withRouteHandler( - async (req: NextRequest, { params }: { params: Promise<{ id: string }> }) => { + async (_req: NextRequest, { params }: { params: Promise<{ id: string; groupId: string }> }) => { const session = await getSession() - if (!session?.user?.id) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } - const { id } = await params - const result = await getPermissionGroupWithAccess(id, session.user.id) + const { id: workspaceId, groupId: id } = await params + + const access = await checkWorkspaceAccess(workspaceId, session.user.id) + if (!access.exists) { + return NextResponse.json({ error: 'Workspace not found' }, { status: 404 }) + } + if (!access.hasAccess) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + } + + const entitled = await isWorkspaceOnEnterprisePlan(workspaceId) + if (!entitled) { + return NextResponse.json( + { error: 'Access Control is an Enterprise feature' }, + { status: 403 } + ) + } - if (!result) { + const group = await loadGroupInWorkspace(id, workspaceId) + if (!group) { return NextResponse.json({ error: 'Permission group not found' }, { status: 404 }) } @@ -73,79 +81,91 @@ const addMemberSchema = z.object({ }) export const POST = withRouteHandler( - async (req: NextRequest, { params }: { params: Promise<{ id: string }> }) => { + async (req: NextRequest, { params }: { params: Promise<{ id: string; groupId: string }> }) => { const session = await getSession() - if (!session?.user?.id) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } - const { id } = await params + const { id: workspaceId, groupId: id } = await params try { - const hasAccess = await hasAccessControlAccess(session.user.id) - if (!hasAccess) { + const isWorkspaceAdmin = await hasWorkspaceAdminAccess(session.user.id, workspaceId) + if (!isWorkspaceAdmin) { + return NextResponse.json({ error: 'Admin permissions required' }, { status: 403 }) + } + + const entitled = await isWorkspaceOnEnterprisePlan(workspaceId) + if (!entitled) { return NextResponse.json( { error: 'Access Control is an Enterprise feature' }, { status: 403 } ) } - const result = await getPermissionGroupWithAccess(id, session.user.id) - - if (!result) { + const group = await loadGroupInWorkspace(id, workspaceId) + if (!group) { return NextResponse.json({ error: 'Permission group not found' }, { status: 404 }) } - if (result.role !== 'admin' && result.role !== 'owner') { - return NextResponse.json({ error: 'Admin or owner permissions required' }, { status: 403 }) - } - const body = await req.json() const { userId } = addMemberSchema.parse(body) - const [orgMember] = await db - .select({ id: member.id, email: user.email }) - .from(member) - .innerJoin(user, eq(member.userId, user.id)) + const [workspaceMember] = await db + .select({ email: user.email }) + .from(permissions) + .innerJoin(user, eq(permissions.userId, user.id)) .where( - and(eq(member.userId, userId), eq(member.organizationId, result.group.organizationId)) + and( + eq(permissions.userId, userId), + eq(permissions.entityType, 'workspace'), + eq(permissions.entityId, workspaceId) + ) ) .limit(1) - if (!orgMember) { + if (!workspaceMember) { return NextResponse.json( - { error: 'User is not a member of this organization' }, + { error: 'User does not have access to this workspace' }, { status: 400 } ) } - const [existingMembership] = await db - .select({ - id: permissionGroupMember.id, - permissionGroupId: permissionGroupMember.permissionGroupId, - }) - .from(permissionGroupMember) - .where(eq(permissionGroupMember.userId, userId)) - .limit(1) + const newMember = await db.transaction(async (tx) => { + const existingInWorkspace = await tx + .select({ + id: permissionGroupMember.id, + permissionGroupId: permissionGroupMember.permissionGroupId, + }) + .from(permissionGroupMember) + .innerJoin( + permissionGroup, + eq(permissionGroupMember.permissionGroupId, permissionGroup.id) + ) + .where( + and( + eq(permissionGroupMember.userId, userId), + eq(permissionGroup.workspaceId, workspaceId) + ) + ) - if (existingMembership?.permissionGroupId === id) { - return NextResponse.json( - { error: 'User is already in this permission group' }, - { status: 409 } - ) - } + if (existingInWorkspace.some((row) => row.permissionGroupId === id)) { + throw new Error('ALREADY_IN_GROUP') + } - const newMember = await db.transaction(async (tx) => { - if (existingMembership) { - await tx - .delete(permissionGroupMember) - .where(eq(permissionGroupMember.id, existingMembership.id)) + if (existingInWorkspace.length > 0) { + await tx.delete(permissionGroupMember).where( + inArray( + permissionGroupMember.id, + existingInWorkspace.map((row) => row.id) + ) + ) } const memberData = { id: generateId(), permissionGroupId: id, + workspaceId, userId, assignedBy: session.user.id, assignedAt: new Date(), @@ -157,23 +177,24 @@ export const POST = withRouteHandler( logger.info('Added member to permission group', { permissionGroupId: id, + workspaceId, userId, assignedBy: session.user.id, }) recordAudit({ - workspaceId: null, + workspaceId, actorId: session.user.id, action: AuditAction.PERMISSION_GROUP_MEMBER_ADDED, resourceType: AuditResourceType.PERMISSION_GROUP, resourceId: id, - resourceName: result.group.name, + resourceName: group.name, actorName: session.user.name ?? undefined, actorEmail: session.user.email ?? undefined, - description: `Added member ${userId} to permission group "${result.group.name}"`, + description: `Added member ${userId} to permission group "${group.name}"`, metadata: { targetUserId: userId, - targetEmail: orgMember.email ?? undefined, + targetEmail: workspaceMember.email ?? undefined, permissionGroupId: id, }, request: req, @@ -184,15 +205,30 @@ export const POST = withRouteHandler( if (error instanceof z.ZodError) { return NextResponse.json({ error: error.errors[0].message }, { status: 400 }) } - if ( - error instanceof Error && - error.message.includes('permission_group_member_user_id_unique') - ) { + if (error instanceof Error && error.message === 'ALREADY_IN_GROUP') { return NextResponse.json( - { error: 'User is already in a permission group' }, + { error: 'User is already in this permission group' }, { status: 409 } ) } + if (getPostgresErrorCode(error) === '23505') { + const constraint = getPostgresConstraintName(error) + if (constraint === PERMISSION_GROUP_MEMBER_CONSTRAINTS.workspaceUser) { + return NextResponse.json( + { + error: + 'User was concurrently added to another group in this workspace. Please refresh and try again.', + }, + { status: 409 } + ) + } + if (constraint === PERMISSION_GROUP_MEMBER_CONSTRAINTS.groupUser) { + return NextResponse.json( + { error: 'User is already in this permission group' }, + { status: 409 } + ) + } + } logger.error('Error adding member to permission group', error) return NextResponse.json({ error: 'Failed to add member' }, { status: 500 }) } @@ -200,14 +236,13 @@ export const POST = withRouteHandler( ) export const DELETE = withRouteHandler( - async (req: NextRequest, { params }: { params: Promise<{ id: string }> }) => { + async (req: NextRequest, { params }: { params: Promise<{ id: string; groupId: string }> }) => { const session = await getSession() - if (!session?.user?.id) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } - const { id } = await params + const { id: workspaceId, groupId: id } = await params const { searchParams } = new URL(req.url) const memberId = searchParams.get('memberId') @@ -216,28 +251,27 @@ export const DELETE = withRouteHandler( } try { - const hasAccess = await hasAccessControlAccess(session.user.id) - if (!hasAccess) { + const isWorkspaceAdmin = await hasWorkspaceAdminAccess(session.user.id, workspaceId) + if (!isWorkspaceAdmin) { + return NextResponse.json({ error: 'Admin permissions required' }, { status: 403 }) + } + + const entitled = await isWorkspaceOnEnterprisePlan(workspaceId) + if (!entitled) { return NextResponse.json( { error: 'Access Control is an Enterprise feature' }, { status: 403 } ) } - const result = await getPermissionGroupWithAccess(id, session.user.id) - - if (!result) { + const group = await loadGroupInWorkspace(id, workspaceId) + if (!group) { return NextResponse.json({ error: 'Permission group not found' }, { status: 404 }) } - if (result.role !== 'admin' && result.role !== 'owner') { - return NextResponse.json({ error: 'Admin or owner permissions required' }, { status: 403 }) - } - const [memberToRemove] = await db .select({ id: permissionGroupMember.id, - permissionGroupId: permissionGroupMember.permissionGroupId, userId: permissionGroupMember.userId, email: user.email, }) @@ -259,20 +293,21 @@ export const DELETE = withRouteHandler( logger.info('Removed member from permission group', { permissionGroupId: id, + workspaceId, memberId, userId: session.user.id, }) recordAudit({ - workspaceId: null, + workspaceId, actorId: session.user.id, action: AuditAction.PERMISSION_GROUP_MEMBER_REMOVED, resourceType: AuditResourceType.PERMISSION_GROUP, resourceId: id, - resourceName: result.group.name, + resourceName: group.name, actorName: session.user.name ?? undefined, actorEmail: session.user.email ?? undefined, - description: `Removed member ${memberToRemove.userId} from permission group "${result.group.name}"`, + description: `Removed member ${memberToRemove.userId} from permission group "${group.name}"`, metadata: { targetUserId: memberToRemove.userId, targetEmail: memberToRemove.email ?? undefined, diff --git a/apps/sim/app/api/permission-groups/[id]/route.ts b/apps/sim/app/api/workspaces/[id]/permission-groups/[groupId]/route.ts similarity index 63% rename from apps/sim/app/api/permission-groups/[id]/route.ts rename to apps/sim/app/api/workspaces/[id]/permission-groups/[groupId]/route.ts index 471caaf73ca..0450d123c41 100644 --- a/apps/sim/app/api/permission-groups/[id]/route.ts +++ b/apps/sim/app/api/workspaces/[id]/permission-groups/[groupId]/route.ts @@ -1,58 +1,34 @@ import { db } from '@sim/db' -import { member, permissionGroup, permissionGroupMember } from '@sim/db/schema' +import { permissionGroup, permissionGroupMember } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' -import { hasAccessControlAccess } from '@/lib/billing' +import { isWorkspaceOnEnterprisePlan } from '@/lib/billing' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { type PermissionGroupConfig, parsePermissionGroupConfig, + permissionGroupConfigSchema, } from '@/lib/permission-groups/types' +import { checkWorkspaceAccess, hasWorkspaceAdminAccess } from '@/lib/workspaces/permissions/utils' -const logger = createLogger('PermissionGroup') - -const configSchema = z.object({ - allowedIntegrations: z.array(z.string()).nullable().optional(), - allowedModelProviders: z.array(z.string()).nullable().optional(), - hideTraceSpans: z.boolean().optional(), - hideKnowledgeBaseTab: z.boolean().optional(), - hideTablesTab: z.boolean().optional(), - hideCopilot: z.boolean().optional(), - hideIntegrationsTab: z.boolean().optional(), - hideSecretsTab: z.boolean().optional(), - hideApiKeysTab: z.boolean().optional(), - hideInboxTab: z.boolean().optional(), - hideEnvironmentTab: z.boolean().optional(), - hideFilesTab: z.boolean().optional(), - disableMcpTools: z.boolean().optional(), - disableCustomTools: z.boolean().optional(), - disableSkills: z.boolean().optional(), - hideTemplates: z.boolean().optional(), - disableInvitations: z.boolean().optional(), - disablePublicApi: z.boolean().optional(), - hideDeployApi: z.boolean().optional(), - hideDeployMcp: z.boolean().optional(), - hideDeployA2a: z.boolean().optional(), - hideDeployChatbot: z.boolean().optional(), - hideDeployTemplate: z.boolean().optional(), -}) +const logger = createLogger('WorkspacePermissionGroup') const updateSchema = z.object({ name: z.string().trim().min(1).max(100).optional(), description: z.string().max(500).nullable().optional(), - config: configSchema.optional(), + config: permissionGroupConfigSchema.optional(), autoAddNewMembers: z.boolean().optional(), }) -async function getPermissionGroupWithAccess(groupId: string, userId: string) { +async function loadGroupInWorkspace(groupId: string, workspaceId: string) { const [group] = await db .select({ id: permissionGroup.id, - organizationId: permissionGroup.organizationId, + workspaceId: permissionGroup.workspaceId, name: permissionGroup.name, description: permissionGroup.description, config: permissionGroup.config, @@ -62,75 +38,80 @@ async function getPermissionGroupWithAccess(groupId: string, userId: string) { autoAddNewMembers: permissionGroup.autoAddNewMembers, }) .from(permissionGroup) - .where(eq(permissionGroup.id, groupId)) - .limit(1) - - if (!group) return null - - const [membership] = await db - .select({ role: member.role }) - .from(member) - .where(and(eq(member.userId, userId), eq(member.organizationId, group.organizationId))) + .where(and(eq(permissionGroup.id, groupId), eq(permissionGroup.workspaceId, workspaceId))) .limit(1) - if (!membership) return null - - return { group, role: membership.role } + return group ?? null } export const GET = withRouteHandler( - async (req: NextRequest, { params }: { params: Promise<{ id: string }> }) => { + async (_req: NextRequest, { params }: { params: Promise<{ id: string; groupId: string }> }) => { const session = await getSession() - if (!session?.user?.id) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } - const { id } = await params - const result = await getPermissionGroupWithAccess(id, session.user.id) + const { id: workspaceId, groupId: id } = await params - if (!result) { + const access = await checkWorkspaceAccess(workspaceId, session.user.id) + if (!access.exists) { + return NextResponse.json({ error: 'Workspace not found' }, { status: 404 }) + } + if (!access.hasAccess) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + } + + const entitled = await isWorkspaceOnEnterprisePlan(workspaceId) + if (!entitled) { + return NextResponse.json( + { error: 'Access Control is an Enterprise feature' }, + { status: 403 } + ) + } + + const group = await loadGroupInWorkspace(id, workspaceId) + + if (!group) { return NextResponse.json({ error: 'Permission group not found' }, { status: 404 }) } return NextResponse.json({ permissionGroup: { - ...result.group, - config: parsePermissionGroupConfig(result.group.config), + ...group, + config: parsePermissionGroupConfig(group.config), }, }) } ) export const PUT = withRouteHandler( - async (req: NextRequest, { params }: { params: Promise<{ id: string }> }) => { + async (req: NextRequest, { params }: { params: Promise<{ id: string; groupId: string }> }) => { const session = await getSession() - if (!session?.user?.id) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } - const { id } = await params + const { id: workspaceId, groupId: id } = await params try { - const hasAccess = await hasAccessControlAccess(session.user.id) - if (!hasAccess) { + const isWorkspaceAdmin = await hasWorkspaceAdminAccess(session.user.id, workspaceId) + if (!isWorkspaceAdmin) { + return NextResponse.json({ error: 'Admin permissions required' }, { status: 403 }) + } + + const entitled = await isWorkspaceOnEnterprisePlan(workspaceId) + if (!entitled) { return NextResponse.json( { error: 'Access Control is an Enterprise feature' }, { status: 403 } ) } - const result = await getPermissionGroupWithAccess(id, session.user.id) - - if (!result) { + const group = await loadGroupInWorkspace(id, workspaceId) + if (!group) { return NextResponse.json({ error: 'Permission group not found' }, { status: 404 }) } - if (result.role !== 'admin' && result.role !== 'owner') { - return NextResponse.json({ error: 'Admin or owner permissions required' }, { status: 403 }) - } - const body = await req.json() const updates = updateSchema.parse(body) @@ -140,7 +121,7 @@ export const PUT = withRouteHandler( .from(permissionGroup) .where( and( - eq(permissionGroup.organizationId, result.group.organizationId), + eq(permissionGroup.workspaceId, workspaceId), eq(permissionGroup.name, updates.name) ) ) @@ -154,7 +135,7 @@ export const PUT = withRouteHandler( } } - const currentConfig = parsePermissionGroupConfig(result.group.config) + const currentConfig = parsePermissionGroupConfig(group.config) const newConfig: PermissionGroupConfig = updates.config ? { ...currentConfig, ...updates.config } : currentConfig @@ -168,7 +149,7 @@ export const PUT = withRouteHandler( .set({ autoAddNewMembers: false, updatedAt: now }) .where( and( - eq(permissionGroup.organizationId, result.group.organizationId), + eq(permissionGroup.workspaceId, workspaceId), eq(permissionGroup.autoAddNewMembers, true) ) ) @@ -195,7 +176,7 @@ export const PUT = withRouteHandler( .limit(1) recordAudit({ - workspaceId: null, + workspaceId, actorId: session.user.id, action: AuditAction.PERMISSION_GROUP_UPDATED, resourceType: AuditResourceType.PERMISSION_GROUP, @@ -205,7 +186,7 @@ export const PUT = withRouteHandler( resourceName: updated.name, description: `Updated permission group "${updated.name}"`, metadata: { - organizationId: result.group.organizationId, + workspaceId, updatedFields: Object.keys(updates).filter( (k) => updates[k as keyof typeof updates] !== undefined ), @@ -230,34 +211,33 @@ export const PUT = withRouteHandler( ) export const DELETE = withRouteHandler( - async (req: NextRequest, { params }: { params: Promise<{ id: string }> }) => { + async (req: NextRequest, { params }: { params: Promise<{ id: string; groupId: string }> }) => { const session = await getSession() - if (!session?.user?.id) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } - const { id } = await params + const { id: workspaceId, groupId: id } = await params try { - const hasAccess = await hasAccessControlAccess(session.user.id) - if (!hasAccess) { + const isWorkspaceAdmin = await hasWorkspaceAdminAccess(session.user.id, workspaceId) + if (!isWorkspaceAdmin) { + return NextResponse.json({ error: 'Admin permissions required' }, { status: 403 }) + } + + const entitled = await isWorkspaceOnEnterprisePlan(workspaceId) + if (!entitled) { return NextResponse.json( { error: 'Access Control is an Enterprise feature' }, { status: 403 } ) } - const result = await getPermissionGroupWithAccess(id, session.user.id) - - if (!result) { + const group = await loadGroupInWorkspace(id, workspaceId) + if (!group) { return NextResponse.json({ error: 'Permission group not found' }, { status: 404 }) } - if (result.role !== 'admin' && result.role !== 'owner') { - return NextResponse.json({ error: 'Admin or owner permissions required' }, { status: 403 }) - } - await db.transaction(async (tx) => { await tx .delete(permissionGroupMember) @@ -265,19 +245,23 @@ export const DELETE = withRouteHandler( await tx.delete(permissionGroup).where(eq(permissionGroup.id, id)) }) - logger.info('Deleted permission group', { permissionGroupId: id, userId: session.user.id }) + logger.info('Deleted permission group', { + permissionGroupId: id, + workspaceId, + userId: session.user.id, + }) recordAudit({ - workspaceId: null, + workspaceId, actorId: session.user.id, action: AuditAction.PERMISSION_GROUP_DELETED, resourceType: AuditResourceType.PERMISSION_GROUP, resourceId: id, actorName: session.user.name ?? undefined, actorEmail: session.user.email ?? undefined, - resourceName: result.group.name, - description: `Deleted permission group "${result.group.name}"`, - metadata: { organizationId: result.group.organizationId }, + resourceName: group.name, + description: `Deleted permission group "${group.name}"`, + metadata: { workspaceId }, request: req, }) diff --git a/apps/sim/app/api/workspaces/[id]/permission-groups/route.ts b/apps/sim/app/api/workspaces/[id]/permission-groups/route.ts new file mode 100644 index 00000000000..1181890bc1e --- /dev/null +++ b/apps/sim/app/api/workspaces/[id]/permission-groups/route.ts @@ -0,0 +1,192 @@ +import { db } from '@sim/db' +import { permissionGroup, permissionGroupMember, user } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { generateId } from '@sim/utils/id' +import { and, count, desc, eq } from 'drizzle-orm' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' +import { getSession } from '@/lib/auth' +import { isWorkspaceOnEnterprisePlan } from '@/lib/billing' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { + DEFAULT_PERMISSION_GROUP_CONFIG, + type PermissionGroupConfig, + parsePermissionGroupConfig, + permissionGroupConfigSchema, +} from '@/lib/permission-groups/types' +import { checkWorkspaceAccess, hasWorkspaceAdminAccess } from '@/lib/workspaces/permissions/utils' + +const logger = createLogger('WorkspacePermissionGroups') + +const createSchema = z.object({ + name: z.string().trim().min(1).max(100), + description: z.string().max(500).optional(), + config: permissionGroupConfigSchema.optional(), + autoAddNewMembers: z.boolean().optional(), +}) + +export const GET = withRouteHandler( + async (_req: NextRequest, { params }: { params: Promise<{ id: string }> }) => { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const { id: workspaceId } = await params + + const access = await checkWorkspaceAccess(workspaceId, session.user.id) + if (!access.exists) { + return NextResponse.json({ error: 'Workspace not found' }, { status: 404 }) + } + if (!access.hasAccess) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + } + + const entitled = await isWorkspaceOnEnterprisePlan(workspaceId) + if (!entitled) { + return NextResponse.json( + { error: 'Access Control is an Enterprise feature' }, + { status: 403 } + ) + } + + const groups = await db + .select({ + id: permissionGroup.id, + name: permissionGroup.name, + description: permissionGroup.description, + config: permissionGroup.config, + createdBy: permissionGroup.createdBy, + createdAt: permissionGroup.createdAt, + updatedAt: permissionGroup.updatedAt, + autoAddNewMembers: permissionGroup.autoAddNewMembers, + creatorName: user.name, + creatorEmail: user.email, + }) + .from(permissionGroup) + .leftJoin(user, eq(permissionGroup.createdBy, user.id)) + .where(eq(permissionGroup.workspaceId, workspaceId)) + .orderBy(desc(permissionGroup.createdAt)) + + const groupsWithCounts = await Promise.all( + groups.map(async (group) => { + const [memberCount] = await db + .select({ count: count() }) + .from(permissionGroupMember) + .where(eq(permissionGroupMember.permissionGroupId, group.id)) + + return { + ...group, + config: parsePermissionGroupConfig(group.config), + memberCount: memberCount?.count ?? 0, + } + }) + ) + + return NextResponse.json({ permissionGroups: groupsWithCounts }) + } +) + +export const POST = withRouteHandler( + async (req: NextRequest, { params }: { params: Promise<{ id: string }> }) => { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const { id: workspaceId } = await params + + try { + const isWorkspaceAdmin = await hasWorkspaceAdminAccess(session.user.id, workspaceId) + if (!isWorkspaceAdmin) { + return NextResponse.json({ error: 'Admin permissions required' }, { status: 403 }) + } + + const entitled = await isWorkspaceOnEnterprisePlan(workspaceId) + if (!entitled) { + return NextResponse.json( + { error: 'Access Control is an Enterprise feature' }, + { status: 403 } + ) + } + + const body = await req.json() + const { name, description, config, autoAddNewMembers } = createSchema.parse(body) + + const existingGroup = await db + .select({ id: permissionGroup.id }) + .from(permissionGroup) + .where(and(eq(permissionGroup.workspaceId, workspaceId), eq(permissionGroup.name, name))) + .limit(1) + + if (existingGroup.length > 0) { + return NextResponse.json( + { error: 'A permission group with this name already exists' }, + { status: 409 } + ) + } + + const groupConfig: PermissionGroupConfig = { + ...DEFAULT_PERMISSION_GROUP_CONFIG, + ...config, + } + + const now = new Date() + const newGroup = { + id: generateId(), + workspaceId, + name, + description: description || null, + config: groupConfig, + createdBy: session.user.id, + createdAt: now, + updatedAt: now, + autoAddNewMembers: autoAddNewMembers || false, + } + + await db.transaction(async (tx) => { + if (autoAddNewMembers) { + await tx + .update(permissionGroup) + .set({ autoAddNewMembers: false, updatedAt: now }) + .where( + and( + eq(permissionGroup.workspaceId, workspaceId), + eq(permissionGroup.autoAddNewMembers, true) + ) + ) + } + await tx.insert(permissionGroup).values(newGroup) + }) + + logger.info('Created permission group', { + permissionGroupId: newGroup.id, + workspaceId, + userId: session.user.id, + }) + + recordAudit({ + workspaceId, + actorId: session.user.id, + action: AuditAction.PERMISSION_GROUP_CREATED, + resourceType: AuditResourceType.PERMISSION_GROUP, + resourceId: newGroup.id, + actorName: session.user.name ?? undefined, + actorEmail: session.user.email ?? undefined, + resourceName: name, + description: `Created permission group "${name}"`, + metadata: { workspaceId, autoAddNewMembers: autoAddNewMembers || false }, + request: req, + }) + + return NextResponse.json({ permissionGroup: newGroup }, { status: 201 }) + } catch (error) { + if (error instanceof z.ZodError) { + return NextResponse.json({ error: error.errors[0].message }, { status: 400 }) + } + logger.error('Error creating permission group', error) + return NextResponse.json({ error: 'Failed to create permission group' }, { status: 500 }) + } + } +) diff --git a/apps/sim/app/api/workspaces/[id]/permissions/route.ts b/apps/sim/app/api/workspaces/[id]/permissions/route.ts index 454dfcac68b..f228f8ef033 100644 --- a/apps/sim/app/api/workspaces/[id]/permissions/route.ts +++ b/apps/sim/app/api/workspaces/[id]/permissions/route.ts @@ -9,10 +9,14 @@ import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { syncWorkspaceEnvCredentials } from '@/lib/credentials/environment' +import { applyWorkspaceAutoAddGroup } from '@/lib/permission-groups/auto-add' import { captureServerEvent } from '@/lib/posthog/server' import { + checkWorkspaceAccess, + getUserEntityPermissions, getUsersWithPermissions, hasWorkspaceAdminAccess, + type PermissionType, } from '@/lib/workspaces/permissions/utils' const logger = createLogger('WorkspacesPermissionsAPI') @@ -45,27 +49,36 @@ export const GET = withRouteHandler( return NextResponse.json({ error: 'Authentication required' }, { status: 401 }) } - const userPermission = await db - .select() - .from(permissions) - .where( - and( - eq(permissions.entityId, workspaceId), - eq(permissions.entityType, 'workspace'), - eq(permissions.userId, session.user.id) - ) - ) - .limit(1) + const isAdmin = await hasWorkspaceAdminAccess(session.user.id, workspaceId) + const access = await checkWorkspaceAccess(workspaceId, session.user.id) + + if (!access.exists) { + return NextResponse.json({ error: 'Workspace not found or access denied' }, { status: 404 }) + } - if (userPermission.length === 0) { + if (!isAdmin && !access.hasAccess) { return NextResponse.json({ error: 'Workspace not found or access denied' }, { status: 404 }) } + const explicitPermission = await getUserEntityPermissions( + session.user.id, + 'workspace', + workspaceId + ) + const viewerPermissionType: PermissionType = isAdmin + ? 'admin' + : (explicitPermission ?? 'read') + const result = await getUsersWithPermissions(workspaceId) return NextResponse.json({ users: result, total: result.length, + viewer: { + userId: session.user.id, + isAdmin, + permissionType: viewerPermissionType, + }, }) } catch (error) { logger.error('Error fetching workspace permissions:', error) @@ -154,6 +167,8 @@ export const PATCH = withRouteHandler( await db.transaction(async (tx) => { for (const update of body.updates) { + const isNew = !permLookup.has(update.userId) + await tx .delete(permissions) .where( @@ -173,6 +188,10 @@ export const PATCH = withRouteHandler( createdAt: new Date(), updatedAt: new Date(), }) + + if (isNew) { + await applyWorkspaceAutoAddGroup(tx, workspaceId, update.userId) + } } }) diff --git a/apps/sim/app/api/workspaces/[id]/route.ts b/apps/sim/app/api/workspaces/[id]/route.ts index b21753647ed..e1fffb08a3c 100644 --- a/apps/sim/app/api/workspaces/[id]/route.ts +++ b/apps/sim/app/api/workspaces/[id]/route.ts @@ -184,6 +184,16 @@ export const PATCH = withRouteHandler( ) } + if (existingWorkspace.workspaceMode === 'personal') { + return NextResponse.json( + { + error: + 'Personal workspaces are always billed to their owner and cannot change billed account.', + }, + { status: 400 } + ) + } + const candidateId = billedAccountUserId const isOwner = candidateId === existingWorkspace.ownerId diff --git a/apps/sim/app/api/workspaces/invitations/route.ts b/apps/sim/app/api/workspaces/invitations/route.ts index 1c01a23b9ad..5f6c848257b 100644 --- a/apps/sim/app/api/workspaces/invitations/route.ts +++ b/apps/sim/app/api/workspaces/invitations/route.ts @@ -69,14 +69,14 @@ export const POST = withRouteHandler(async (req: NextRequest) => { } try { - await validateInvitationsAllowed(session.user.id) - const { workspaceId, email, permission = 'read' } = await req.json() if (!workspaceId || !email) { return NextResponse.json({ error: 'Workspace ID and email are required' }, { status: 400 }) } + await validateInvitationsAllowed(session.user.id, workspaceId) + const validPermissions: PermissionType[] = ['admin', 'write', 'read'] if (!validPermissions.includes(permission)) { return NextResponse.json( diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/credentials/credentials-manager.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/credentials/credentials-manager.tsx index 970a5b3d7bf..42cda8def1c 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/credentials/credentials-manager.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/credentials/credentials-manager.tsx @@ -399,12 +399,7 @@ export function CredentialsManager() { const { data: workspacePermissions } = useWorkspacePermissionsQuery(workspaceId || null) const queryClient = useQueryClient() - const isWorkspaceAdmin = useMemo(() => { - const userId = session?.user?.id - if (!userId || !workspacePermissions?.users) return false - const currentUser = workspacePermissions.users.find((user) => user.userId === userId) - return currentUser?.permissionType === 'admin' - }, [session?.user?.id, workspacePermissions?.users]) + const isWorkspaceAdmin = workspacePermissions?.viewer?.isAdmin ?? false const isLoading = isPersonalLoading || isWorkspaceLoading const variables = useMemo(() => personalEnvData || {}, [personalEnvData]) diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/subscription/subscription.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/subscription/subscription.tsx index 377d17e8372..97b7f1d15b8 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/subscription/subscription.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/subscription/subscription.tsx @@ -345,9 +345,10 @@ export function Subscription() { const isCritical = isBlocked || usage.percentUsed >= USAGE_THRESHOLDS.CRITICAL const billedAccountUserId = workspaceData?.settings?.workspace?.billedAccountUserId ?? null + const workspaceMode = workspaceData?.settings?.workspace?.workspaceMode ?? null const isOrganizationWorkspace = - workspaceData?.settings?.workspace?.organizationId != null && - workspaceData?.settings?.workspace?.workspaceMode === 'organization' + workspaceData?.settings?.workspace?.organizationId != null && workspaceMode === 'organization' + const isGrandfatheredSharedWorkspace = workspaceMode === 'grandfathered_shared' const workspaceAdmins: WorkspaceAdmin[] = workspaceData?.permissions?.users?.filter( (user: WorkspaceAdmin) => user.permissionType === 'admin' @@ -1073,7 +1074,7 @@ export function Subscription() { )} - {!isLoading && isTeamAdmin && !isOrganizationWorkspace && ( + {!isLoading && isTeamAdmin && isGrandfatheredSharedWorkspace && (
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx index 4e201d93590..d7229b160ae 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx @@ -484,10 +484,6 @@ export function DeployModal({ {!permissionConfig.hideDeployChatbot && ( Chat )} - {/* Form */} - {/* {!permissionConfig.hideDeployTemplate && ( - Template - )} */} diff --git a/apps/sim/ee/access-control/components/access-control.tsx b/apps/sim/ee/access-control/components/access-control.tsx index 65cc642e13d..3cea9b0bf8d 100644 --- a/apps/sim/ee/access-control/components/access-control.tsx +++ b/apps/sim/ee/access-control/components/access-control.tsx @@ -3,6 +3,7 @@ import { useCallback, useMemo, useState } from 'react' import { createLogger } from '@sim/logger' import { Plus, Search } from 'lucide-react' +import { useParams } from 'next/navigation' import { Avatar, AvatarFallback, @@ -24,12 +25,9 @@ import { Switch, } from '@/components/emcn' import { Input as BaseInput } from '@/components/ui' -import { useSession } from '@/lib/auth/auth-client' -import { getSubscriptionAccessState } from '@/lib/billing/client' -import { isBillingEnabled } from '@/lib/core/config/feature-flags' +import { getEnv, isTruthy } from '@/lib/core/config/env' import type { PermissionGroupConfig } from '@/lib/permission-groups/types' import { getUserColor } from '@/lib/workspaces/colors' -import { getUserRole } from '@/lib/workspaces/organization' import { getAllBlocks } from '@/blocks' import { type PermissionGroup, @@ -40,16 +38,16 @@ import { usePermissionGroups, useRemovePermissionGroupMember, useUpdatePermissionGroup, + useUserPermissionConfig, } from '@/ee/access-control/hooks/permission-groups' import { useBlacklistedProviders } from '@/hooks/queries/allowed-providers' -import { useOrganization, useOrganizations } from '@/hooks/queries/organization' -import { useSubscriptionData } from '@/hooks/queries/subscription' +import { useWorkspacePermissionsQuery } from '@/hooks/queries/workspace' import { PROVIDER_DEFINITIONS } from '@/providers/models' import { getAllProviderIds } from '@/providers/utils' const logger = createLogger('AccessControl') -interface OrgMember { +interface WorkspaceMemberOption { userId: string user: { name: string | null @@ -61,11 +59,12 @@ interface OrgMember { interface AddMembersModalProps { open: boolean onOpenChange: (open: boolean) => void - availableMembers: OrgMember[] + availableMembers: WorkspaceMemberOption[] selectedMemberIds: Set setSelectedMemberIds: React.Dispatch>> onAddMembers: () => void isAdding: boolean + errorMessage: string | null } function AddMembersModal({ @@ -76,6 +75,7 @@ function AddMembersModal({ setSelectedMemberIds, onAddMembers, isAdding, + errorMessage, }: AddMembersModalProps) { const [searchTerm, setSearchTerm] = useState('') @@ -136,7 +136,7 @@ function AddMembersModal({ {availableMembers.length === 0 ? (

- All organization members are already in this group. + All workspace members are already in this group.

) : (
@@ -201,6 +201,9 @@ function AddMembersModal({
)} + {errorMessage && ( +

{errorMessage}

+ )}
{ + setShowAddMembersModal(open) + if (!open) setAddMembersError(null) + }} availableMembers={availableMembersToAdd} selectedMemberIds={selectedMemberIds} setSelectedMemberIds={setSelectedMemberIds} onAddMembers={handleAddSelectedMembers} isAdding={bulkAddMembers.isPending} + errorMessage={addMembersError} /> ) @@ -1331,7 +1326,7 @@ export function AccessControl() { onCheckedChange={(checked) => setNewGroupAutoAdd(checked === true)} /> {createError &&

{createError}

} diff --git a/apps/sim/ee/access-control/hooks/permission-groups.ts b/apps/sim/ee/access-control/hooks/permission-groups.ts index 91f838ced2d..cd7bdf6b369 100644 --- a/apps/sim/ee/access-control/hooks/permission-groups.ts +++ b/apps/sim/ee/access-control/hooks/permission-groups.ts @@ -31,48 +31,37 @@ export interface UserPermissionConfig { permissionGroupId: string | null groupName: string | null config: PermissionGroupConfig | null + entitled: boolean } export const permissionGroupKeys = { all: ['permissionGroups'] as const, - list: (organizationId?: string) => - ['permissionGroups', 'list', organizationId ?? 'none'] as const, - detail: (id?: string) => ['permissionGroups', 'detail', id ?? 'none'] as const, - members: (id?: string) => ['permissionGroups', 'members', id ?? 'none'] as const, - userConfig: (organizationId?: string) => - ['permissionGroups', 'userConfig', organizationId ?? 'none'] as const, + lists: () => [...permissionGroupKeys.all, 'list'] as const, + list: (workspaceId?: string) => [...permissionGroupKeys.lists(), workspaceId ?? ''] as const, + details: () => [...permissionGroupKeys.all, 'detail'] as const, + detail: (workspaceId?: string, id?: string) => + [...permissionGroupKeys.details(), workspaceId ?? '', id ?? ''] as const, + members: (workspaceId?: string, id?: string) => + [...permissionGroupKeys.detail(workspaceId, id), 'members'] as const, + userConfig: (workspaceId?: string) => + [...permissionGroupKeys.all, 'userConfig', workspaceId ?? ''] as const, } interface PermissionGroupsResponse { permissionGroups?: PermissionGroup[] } -export function usePermissionGroups(organizationId?: string, enabled = true) { +export function usePermissionGroups(workspaceId?: string, enabled = true) { return useQuery({ - queryKey: permissionGroupKeys.list(organizationId), - queryFn: async () => { - const data = await fetchJson('/api/permission-groups', { - searchParams: { organizationId: organizationId ?? '' }, - }) + queryKey: permissionGroupKeys.list(workspaceId), + queryFn: async ({ signal }) => { + const data = await fetchJson( + `/api/workspaces/${workspaceId}/permission-groups`, + { signal } + ) return data.permissionGroups ?? [] }, - enabled: Boolean(organizationId) && enabled, - staleTime: 60 * 1000, - }) -} - -interface PermissionGroupDetailResponse { - permissionGroup?: PermissionGroup -} - -export function usePermissionGroup(id?: string, enabled = true) { - return useQuery({ - queryKey: permissionGroupKeys.detail(id), - queryFn: async () => { - const data = await fetchJson(`/api/permission-groups/${id}`) - return data.permissionGroup ?? null - }, - enabled: Boolean(id) && enabled, + enabled: Boolean(workspaceId) && enabled, staleTime: 60 * 1000, }) } @@ -81,36 +70,38 @@ interface MembersResponse { members?: PermissionGroupMember[] } -export function usePermissionGroupMembers(permissionGroupId?: string) { +export function usePermissionGroupMembers(workspaceId?: string, permissionGroupId?: string) { return useQuery({ - queryKey: permissionGroupKeys.members(permissionGroupId), - queryFn: async () => { + queryKey: permissionGroupKeys.members(workspaceId, permissionGroupId), + queryFn: async ({ signal }) => { const data = await fetchJson( - `/api/permission-groups/${permissionGroupId}/members` + `/api/workspaces/${workspaceId}/permission-groups/${permissionGroupId}/members`, + { signal } ) return data.members ?? [] }, - enabled: Boolean(permissionGroupId), + enabled: Boolean(workspaceId) && Boolean(permissionGroupId), staleTime: 30 * 1000, }) } -export function useUserPermissionConfig(organizationId?: string) { +export function useUserPermissionConfig(workspaceId?: string) { return useQuery({ - queryKey: permissionGroupKeys.userConfig(organizationId), - queryFn: async () => { + queryKey: permissionGroupKeys.userConfig(workspaceId), + queryFn: async ({ signal }) => { const data = await fetchJson('/api/permission-groups/user', { - searchParams: { organizationId: organizationId ?? '' }, + searchParams: { workspaceId: workspaceId ?? '' }, + signal, }) return data }, - enabled: Boolean(organizationId), + enabled: Boolean(workspaceId), staleTime: 60 * 1000, }) } export interface CreatePermissionGroupData { - organizationId: string + workspaceId: string name: string description?: string config?: Partial @@ -121,8 +112,8 @@ export function useCreatePermissionGroup() { const queryClient = useQueryClient() return useMutation({ - mutationFn: async (data: CreatePermissionGroupData) => { - const response = await fetch('/api/permission-groups', { + mutationFn: async ({ workspaceId, ...data }: CreatePermissionGroupData) => { + const response = await fetch(`/api/workspaces/${workspaceId}/permission-groups`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), @@ -135,7 +126,7 @@ export function useCreatePermissionGroup() { }, onSuccess: (_data, variables) => { queryClient.invalidateQueries({ - queryKey: permissionGroupKeys.list(variables.organizationId), + queryKey: permissionGroupKeys.list(variables.workspaceId), }) }, }) @@ -143,7 +134,7 @@ export function useCreatePermissionGroup() { export interface UpdatePermissionGroupData { id: string - organizationId: string + workspaceId: string name?: string description?: string | null config?: Partial @@ -154,8 +145,8 @@ export function useUpdatePermissionGroup() { const queryClient = useQueryClient() return useMutation({ - mutationFn: async ({ id, ...data }: UpdatePermissionGroupData) => { - const response = await fetch(`/api/permission-groups/${id}`, { + mutationFn: async ({ id, workspaceId, ...data }: UpdatePermissionGroupData) => { + const response = await fetch(`/api/workspaces/${workspaceId}/permission-groups/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), @@ -168,27 +159,32 @@ export function useUpdatePermissionGroup() { }, onSuccess: (_data, variables) => { queryClient.invalidateQueries({ - queryKey: permissionGroupKeys.list(variables.organizationId), + queryKey: permissionGroupKeys.list(variables.workspaceId), + }) + queryClient.invalidateQueries({ + queryKey: permissionGroupKeys.detail(variables.workspaceId, variables.id), + }) + queryClient.invalidateQueries({ + queryKey: permissionGroupKeys.userConfig(variables.workspaceId), }) - queryClient.invalidateQueries({ queryKey: permissionGroupKeys.detail(variables.id) }) - queryClient.invalidateQueries({ queryKey: ['permissionGroups', 'userConfig'] }) }, }) } export interface DeletePermissionGroupParams { permissionGroupId: string - organizationId: string + workspaceId: string } export function useDeletePermissionGroup() { const queryClient = useQueryClient() return useMutation({ - mutationFn: async ({ permissionGroupId }: DeletePermissionGroupParams) => { - const response = await fetch(`/api/permission-groups/${permissionGroupId}`, { - method: 'DELETE', - }) + mutationFn: async ({ permissionGroupId, workspaceId }: DeletePermissionGroupParams) => { + const response = await fetch( + `/api/workspaces/${workspaceId}/permission-groups/${permissionGroupId}`, + { method: 'DELETE' } + ) if (!response.ok) { const result = await response.json() throw new Error(result.error || 'Failed to delete permission group') @@ -197,34 +193,11 @@ export function useDeletePermissionGroup() { }, onSuccess: (_data, variables) => { queryClient.invalidateQueries({ - queryKey: permissionGroupKeys.list(variables.organizationId), + queryKey: permissionGroupKeys.list(variables.workspaceId), }) - queryClient.invalidateQueries({ queryKey: ['permissionGroups', 'userConfig'] }) - }, - }) -} - -export function useAddPermissionGroupMember() { - const queryClient = useQueryClient() - - return useMutation({ - mutationFn: async (data: { permissionGroupId: string; userId: string }) => { - const response = await fetch(`/api/permission-groups/${data.permissionGroupId}/members`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ userId: data.userId }), - }) - if (!response.ok) { - const result = await response.json() - throw new Error(result.error || 'Failed to add member') - } - return response.json() - }, - onSuccess: (_data, variables) => { queryClient.invalidateQueries({ - queryKey: permissionGroupKeys.members(variables.permissionGroupId), + queryKey: permissionGroupKeys.userConfig(variables.workspaceId), }) - queryClient.invalidateQueries({ queryKey: permissionGroupKeys.all }) }, }) } @@ -233,9 +206,13 @@ export function useRemovePermissionGroupMember() { const queryClient = useQueryClient() return useMutation({ - mutationFn: async (data: { permissionGroupId: string; memberId: string }) => { + mutationFn: async (data: { + workspaceId: string + permissionGroupId: string + memberId: string + }) => { const response = await fetch( - `/api/permission-groups/${data.permissionGroupId}/members?memberId=${data.memberId}`, + `/api/workspaces/${data.workspaceId}/permission-groups/${data.permissionGroupId}/members?memberId=${data.memberId}`, { method: 'DELETE' } ) if (!response.ok) { @@ -246,30 +223,36 @@ export function useRemovePermissionGroupMember() { }, onSuccess: (_data, variables) => { queryClient.invalidateQueries({ - queryKey: permissionGroupKeys.members(variables.permissionGroupId), + queryKey: permissionGroupKeys.members(variables.workspaceId, variables.permissionGroupId), + }) + queryClient.invalidateQueries({ queryKey: permissionGroupKeys.list(variables.workspaceId) }) + queryClient.invalidateQueries({ + queryKey: permissionGroupKeys.userConfig(variables.workspaceId), }) - queryClient.invalidateQueries({ queryKey: permissionGroupKeys.all }) - queryClient.invalidateQueries({ queryKey: ['permissionGroups', 'userConfig'] }) }, }) } export interface BulkAddMembersData { + workspaceId: string permissionGroupId: string userIds?: string[] - addAllOrgMembers?: boolean + addAllWorkspaceMembers?: boolean } export function useBulkAddPermissionGroupMembers() { const queryClient = useQueryClient() return useMutation({ - mutationFn: async ({ permissionGroupId, ...data }: BulkAddMembersData) => { - const response = await fetch(`/api/permission-groups/${permissionGroupId}/members/bulk`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data), - }) + mutationFn: async ({ workspaceId, permissionGroupId, ...data }: BulkAddMembersData) => { + const response = await fetch( + `/api/workspaces/${workspaceId}/permission-groups/${permissionGroupId}/members/bulk`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + } + ) if (!response.ok) { const result = await response.json() throw new Error(result.error || 'Failed to add members') @@ -278,10 +261,12 @@ export function useBulkAddPermissionGroupMembers() { }, onSuccess: (_data, variables) => { queryClient.invalidateQueries({ - queryKey: permissionGroupKeys.members(variables.permissionGroupId), + queryKey: permissionGroupKeys.members(variables.workspaceId, variables.permissionGroupId), + }) + queryClient.invalidateQueries({ queryKey: permissionGroupKeys.list(variables.workspaceId) }) + queryClient.invalidateQueries({ + queryKey: permissionGroupKeys.userConfig(variables.workspaceId), }) - queryClient.invalidateQueries({ queryKey: permissionGroupKeys.all }) - queryClient.invalidateQueries({ queryKey: ['permissionGroups', 'userConfig'] }) }, }) } diff --git a/apps/sim/ee/access-control/utils/permission-check.test.ts b/apps/sim/ee/access-control/utils/permission-check.test.ts index 1d6c5d0ef09..0c0a3934399 100644 --- a/apps/sim/ee/access-control/utils/permission-check.test.ts +++ b/apps/sim/ee/access-control/utils/permission-check.test.ts @@ -6,8 +6,9 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' const { DEFAULT_PERMISSION_GROUP_CONFIG, mockGetAllowedIntegrationsFromEnv, - mockIsOrganizationOnEnterprisePlan, + mockIsWorkspaceOnEnterprisePlan, mockGetProviderFromModel, + mockDbGroupMembership, } = vi.hoisted(() => ({ DEFAULT_PERMISSION_GROUP_CONFIG: { allowedIntegrations: null, @@ -20,12 +21,10 @@ const { hideSecretsTab: false, hideApiKeysTab: false, hideInboxTab: false, - hideEnvironmentTab: false, hideFilesTab: false, disableMcpTools: false, disableCustomTools: false, disableSkills: false, - hideTemplates: false, disableInvitations: false, disablePublicApi: false, hideDeployApi: false, @@ -35,18 +34,48 @@ const { hideDeployTemplate: false, }, mockGetAllowedIntegrationsFromEnv: vi.fn<() => string[] | null>(), - mockIsOrganizationOnEnterprisePlan: vi.fn<() => Promise>(), + mockIsWorkspaceOnEnterprisePlan: vi.fn<() => Promise>(), mockGetProviderFromModel: vi.fn<(model: string) => string>(), + mockDbGroupMembership: { value: [] as Array<{ config: Record }> }, +})) + +vi.mock('@sim/db', () => ({ + db: { + select: vi.fn().mockImplementation(() => { + const chain: Record = {} + chain.from = vi.fn().mockReturnValue(chain) + chain.innerJoin = vi.fn().mockReturnValue(chain) + chain.where = vi.fn().mockReturnValue(chain) + chain.orderBy = vi.fn().mockReturnValue(chain) + chain.limit = vi.fn().mockImplementation(() => Promise.resolve(mockDbGroupMembership.value)) + return chain + }), + }, +})) + +vi.mock('@sim/db/schema', () => ({ + permissionGroup: {}, + permissionGroupMember: {}, +})) + +vi.mock('drizzle-orm', () => ({ + and: vi.fn(), + eq: vi.fn(), + asc: vi.fn(), })) vi.mock('@/lib/billing', () => ({ - isOrganizationOnEnterprisePlan: mockIsOrganizationOnEnterprisePlan, + isWorkspaceOnEnterprisePlan: mockIsWorkspaceOnEnterprisePlan, })) + vi.mock('@/lib/core/config/feature-flags', () => ({ getAllowedIntegrationsFromEnv: mockGetAllowedIntegrationsFromEnv, - isAccessControlEnabled: false, - isHosted: false, + isAccessControlEnabled: true, + isHosted: true, + isInvitationsDisabled: false, + isPublicApiDisabled: false, })) + vi.mock('@/lib/permission-groups/types', () => ({ DEFAULT_PERMISSION_GROUP_CONFIG, parsePermissionGroupConfig: (config: unknown) => { @@ -54,14 +83,22 @@ vi.mock('@/lib/permission-groups/types', () => ({ return { ...DEFAULT_PERMISSION_GROUP_CONFIG, ...config } }, })) + vi.mock('@/providers/utils', () => ({ getProviderFromModel: mockGetProviderFromModel, })) import { + assertPermissionsAllowed, + CustomToolsNotAllowedError, getUserPermissionConfig, IntegrationNotAllowedError, + McpToolsNotAllowedError, + ProviderNotAllowedError, + SkillsNotAllowedError, validateBlockType, + validateMcpToolsAllowed, + validateModelProvider, } from './permission-check' describe('IntegrationNotAllowedError', () => { @@ -83,88 +120,32 @@ describe('IntegrationNotAllowedError', () => { describe('getUserPermissionConfig', () => { beforeEach(() => { vi.clearAllMocks() + mockDbGroupMembership.value = [] }) - it('returns null when no env allowlist is configured', async () => { - mockGetAllowedIntegrationsFromEnv.mockReturnValue(null) - - const config = await getUserPermissionConfig('user-123') - - expect(config).toBeNull() - }) - - it('returns config with env allowlist when configured', async () => { - mockGetAllowedIntegrationsFromEnv.mockReturnValue(['slack', 'gmail']) - - const config = await getUserPermissionConfig('user-123') - - expect(config).not.toBeNull() - expect(config!.allowedIntegrations).toEqual(['slack', 'gmail']) - }) - - it('preserves default values for non-allowlist fields', async () => { + it('returns env allowlist config when access control is disabled locally', async () => { mockGetAllowedIntegrationsFromEnv.mockReturnValue(['slack']) - const config = await getUserPermissionConfig('user-123') + const config = await getUserPermissionConfig('user-123', 'workspace-1') - expect(config!.disableMcpTools).toBe(false) - expect(config!.allowedModelProviders).toBeNull() - }) -}) - -describe('env allowlist fallback when userId is absent', () => { - beforeEach(() => { - vi.clearAllMocks() + expect(config).not.toBeNull() + expect(config!.allowedIntegrations).toEqual(['slack']) }) - it('returns null allowlist when no userId and no env allowlist', async () => { + it('returns env allowlist config when workspace is not on enterprise plan', async () => { mockGetAllowedIntegrationsFromEnv.mockReturnValue(null) + mockIsWorkspaceOnEnterprisePlan.mockResolvedValue(false) - const userId: string | undefined = undefined - const permissionConfig = userId ? await getUserPermissionConfig(userId) : null - const allowedIntegrations = - permissionConfig?.allowedIntegrations ?? mockGetAllowedIntegrationsFromEnv() + const config = await getUserPermissionConfig('user-123', 'workspace-1') - expect(allowedIntegrations).toBeNull() - }) - - it('falls back to env allowlist when no userId is provided', async () => { - mockGetAllowedIntegrationsFromEnv.mockReturnValue(['slack', 'gmail']) - - const userId: string | undefined = undefined - const permissionConfig = userId ? await getUserPermissionConfig(userId) : null - const allowedIntegrations = - permissionConfig?.allowedIntegrations ?? mockGetAllowedIntegrationsFromEnv() - - expect(allowedIntegrations).toEqual(['slack', 'gmail']) - }) - - it('env allowlist filters block types when userId is absent', async () => { - mockGetAllowedIntegrationsFromEnv.mockReturnValue(['slack', 'gmail']) - - const userId: string | undefined = undefined - const permissionConfig = userId ? await getUserPermissionConfig(userId) : null - const allowedIntegrations = - permissionConfig?.allowedIntegrations ?? mockGetAllowedIntegrationsFromEnv() - - expect(allowedIntegrations).not.toBeNull() - expect(allowedIntegrations!.includes('slack')).toBe(true) - expect(allowedIntegrations!.includes('discord')).toBe(false) - }) - - it('uses permission config when userId is present, ignoring env fallback', async () => { - mockGetAllowedIntegrationsFromEnv.mockReturnValue(['slack', 'gmail']) - - const config = await getUserPermissionConfig('user-123') - - expect(config).not.toBeNull() - expect(config!.allowedIntegrations).toEqual(['slack', 'gmail']) + expect(config).toBeNull() }) }) describe('validateBlockType', () => { beforeEach(() => { vi.clearAllMocks() + mockDbGroupMembership.value = [] }) describe('when no env allowlist is configured', () => { @@ -173,15 +154,15 @@ describe('validateBlockType', () => { }) it('allows any block type', async () => { - await validateBlockType(undefined, 'google_drive') + await validateBlockType(undefined, undefined, 'google_drive') }) it('allows multi-word block types', async () => { - await validateBlockType(undefined, 'microsoft_excel') + await validateBlockType(undefined, undefined, 'microsoft_excel') }) it('always allows start_trigger', async () => { - await validateBlockType(undefined, 'start_trigger') + await validateBlockType(undefined, undefined, 'start_trigger') }) }) @@ -195,71 +176,156 @@ describe('validateBlockType', () => { }) it('allows block types on the allowlist', async () => { - await validateBlockType(undefined, 'slack') - await validateBlockType(undefined, 'google_drive') - await validateBlockType(undefined, 'microsoft_excel') + await validateBlockType(undefined, undefined, 'slack') + await validateBlockType(undefined, undefined, 'google_drive') + await validateBlockType(undefined, undefined, 'microsoft_excel') }) it('rejects block types not on the allowlist', async () => { - await expect(validateBlockType(undefined, 'discord')).rejects.toThrow( + await expect(validateBlockType(undefined, undefined, 'discord')).rejects.toThrow( IntegrationNotAllowedError ) }) it('always allows start_trigger regardless of allowlist', async () => { - await validateBlockType(undefined, 'start_trigger') + await validateBlockType(undefined, undefined, 'start_trigger') }) it('matches case-insensitively', async () => { - await validateBlockType(undefined, 'Slack') - await validateBlockType(undefined, 'GOOGLE_DRIVE') + await validateBlockType(undefined, undefined, 'Slack') + await validateBlockType(undefined, undefined, 'GOOGLE_DRIVE') }) it('includes env reason in error when env allowlist is the source', async () => { - await expect(validateBlockType(undefined, 'discord')).rejects.toThrow(/ALLOWED_INTEGRATIONS/) + await expect(validateBlockType(undefined, undefined, 'discord')).rejects.toThrow( + /ALLOWED_INTEGRATIONS/ + ) }) - it('includes env reason even when userId is present if env is the source', async () => { - await expect(validateBlockType('user-123', 'discord')).rejects.toThrow(/ALLOWED_INTEGRATIONS/) + it('includes env reason even when a workspace is in context', async () => { + await expect(validateBlockType('user-123', 'workspace-1', 'discord')).rejects.toThrow( + /ALLOWED_INTEGRATIONS/ + ) }) }) }) -describe('service ID to block type normalization', () => { - it.concurrent('hyphenated service IDs match underscore block types after normalization', () => { - const allowedBlockTypes = [ - 'google_drive', - 'microsoft_excel', - 'microsoft_teams', - 'google_sheets', - 'google_docs', - 'google_calendar', - 'google_forms', - 'microsoft_planner', - ] - const serviceIds = [ - 'google-drive', - 'microsoft-excel', - 'microsoft-teams', - 'google-sheets', - 'google-docs', - 'google-calendar', - 'google-forms', - 'microsoft-planner', - ] - - for (const serviceId of serviceIds) { - const normalized = serviceId.replace(/-/g, '_') - expect(allowedBlockTypes).toContain(normalized) - } +describe('validateModelProvider', () => { + beforeEach(() => { + vi.clearAllMocks() + mockGetAllowedIntegrationsFromEnv.mockReturnValue(null) + mockIsWorkspaceOnEnterprisePlan.mockResolvedValue(true) + }) + + it('no-ops when user or workspace is missing', async () => { + await validateModelProvider(undefined, 'workspace-1', 'gpt-4') + await validateModelProvider('user-123', undefined, 'gpt-4') + }) + + it('throws ProviderNotAllowedError when provider is not in allowlist', async () => { + mockDbGroupMembership.value = [{ config: { allowedModelProviders: ['anthropic'] } }] + mockGetProviderFromModel.mockReturnValue('openai') + + await expect(validateModelProvider('user-123', 'workspace-1', 'gpt-4')).rejects.toBeInstanceOf( + ProviderNotAllowedError + ) + }) + + it('allows when provider is on the allowlist', async () => { + mockDbGroupMembership.value = [{ config: { allowedModelProviders: ['anthropic', 'openai'] } }] + mockGetProviderFromModel.mockReturnValue('openai') + + await validateModelProvider('user-123', 'workspace-1', 'gpt-4') + }) +}) + +describe('validateMcpToolsAllowed', () => { + beforeEach(() => { + vi.clearAllMocks() + mockGetAllowedIntegrationsFromEnv.mockReturnValue(null) + mockIsWorkspaceOnEnterprisePlan.mockResolvedValue(true) + }) + + it('throws McpToolsNotAllowedError when disableMcpTools is set', async () => { + mockDbGroupMembership.value = [{ config: { disableMcpTools: true } }] + + await expect(validateMcpToolsAllowed('user-123', 'workspace-1')).rejects.toBeInstanceOf( + McpToolsNotAllowedError + ) + }) + + it('no-ops when disableMcpTools is false', async () => { + mockDbGroupMembership.value = [{ config: {} }] + + await validateMcpToolsAllowed('user-123', 'workspace-1') + }) +}) + +describe('assertPermissionsAllowed', () => { + beforeEach(() => { + vi.clearAllMocks() + mockGetAllowedIntegrationsFromEnv.mockReturnValue(null) + mockIsWorkspaceOnEnterprisePlan.mockResolvedValue(true) + }) + + it('throws ProviderNotAllowedError when model provider is blocked', async () => { + mockDbGroupMembership.value = [{ config: { allowedModelProviders: ['anthropic'] } }] + mockGetProviderFromModel.mockReturnValue('openai') + + await expect( + assertPermissionsAllowed({ + userId: 'user-123', + workspaceId: 'workspace-1', + model: 'gpt-4', + }) + ).rejects.toBeInstanceOf(ProviderNotAllowedError) }) - it.concurrent('single-word service IDs are unaffected by normalization', () => { - const serviceIds = ['slack', 'gmail', 'notion', 'discord', 'jira', 'trello'] + it('throws IntegrationNotAllowedError when block type is blocked', async () => { + mockDbGroupMembership.value = [{ config: { allowedIntegrations: ['slack'] } }] - for (const serviceId of serviceIds) { - const normalized = serviceId.replace(/-/g, '_') - expect(normalized).toBe(serviceId) - } + await expect( + assertPermissionsAllowed({ + userId: 'user-123', + workspaceId: 'workspace-1', + blockType: 'discord', + }) + ).rejects.toBeInstanceOf(IntegrationNotAllowedError) + }) + + it('throws CustomToolsNotAllowedError when custom tools are disabled', async () => { + mockDbGroupMembership.value = [{ config: { disableCustomTools: true } }] + + await expect( + assertPermissionsAllowed({ + userId: 'user-123', + workspaceId: 'workspace-1', + toolKind: 'custom', + }) + ).rejects.toBeInstanceOf(CustomToolsNotAllowedError) + }) + + it('throws SkillsNotAllowedError when skills are disabled', async () => { + mockDbGroupMembership.value = [{ config: { disableSkills: true } }] + + await expect( + assertPermissionsAllowed({ + userId: 'user-123', + workspaceId: 'workspace-1', + toolKind: 'skill', + }) + ).rejects.toBeInstanceOf(SkillsNotAllowedError) + }) + + it('passes when the workspace has no blocking config', async () => { + mockDbGroupMembership.value = [] + + await assertPermissionsAllowed({ + userId: 'user-123', + workspaceId: 'workspace-1', + model: 'gpt-4', + blockType: 'slack', + toolKind: 'mcp', + }) }) }) diff --git a/apps/sim/ee/access-control/utils/permission-check.ts b/apps/sim/ee/access-control/utils/permission-check.ts index 376177077c7..a95d23becaa 100644 --- a/apps/sim/ee/access-control/utils/permission-check.ts +++ b/apps/sim/ee/access-control/utils/permission-check.ts @@ -1,12 +1,14 @@ import { db } from '@sim/db' -import { member, permissionGroup, permissionGroupMember } from '@sim/db/schema' +import { permissionGroup, permissionGroupMember, workspace } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { and, eq } from 'drizzle-orm' -import { isOrganizationOnEnterprisePlan } from '@/lib/billing' +import { and, asc, eq, sql } from 'drizzle-orm' +import { isWorkspaceOnEnterprisePlan } from '@/lib/billing' import { getAllowedIntegrationsFromEnv, isAccessControlEnabled, isHosted, + isInvitationsDisabled, + isPublicApiDisabled, } from '@/lib/core/config/feature-flags' import { DEFAULT_PERMISSION_GROUP_CONFIG, @@ -100,24 +102,23 @@ function mergeEnvAllowlist(config: PermissionGroupConfig | null): PermissionGrou return { ...config, allowedIntegrations: merged } } +/** + * Resolve the effective permission-group config for a user in the context of a + * specific workspace. Returns `null` when the workspace isn't on an enterprise + * plan or the user isn't a member of any permission group in that workspace. + * + * The env-level integration allowlist is always merged last so self-hosted + * deployments can constrain integrations without touching the DB. + */ export async function getUserPermissionConfig( - userId: string + userId: string, + workspaceId: string ): Promise { if (!isHosted && !isAccessControlEnabled) { return mergeEnvAllowlist(null) } - const [membership] = await db - .select({ organizationId: member.organizationId }) - .from(member) - .where(eq(member.userId, userId)) - .limit(1) - - if (!membership) { - return mergeEnvAllowlist(null) - } - - const isEnterprise = await isOrganizationOnEnterprisePlan(membership.organizationId) + const isEnterprise = await isWorkspaceOnEnterprisePlan(workspaceId) if (!isEnterprise) { return mergeEnvAllowlist(null) } @@ -127,11 +128,9 @@ export async function getUserPermissionConfig( .from(permissionGroupMember) .innerJoin(permissionGroup, eq(permissionGroupMember.permissionGroupId, permissionGroup.id)) .where( - and( - eq(permissionGroupMember.userId, userId), - eq(permissionGroup.organizationId, membership.organizationId) - ) + and(eq(permissionGroupMember.userId, userId), eq(permissionGroup.workspaceId, workspaceId)) ) + .orderBy(asc(permissionGroup.createdAt), asc(permissionGroup.id)) .limit(1) if (!groupMembership) { @@ -141,12 +140,18 @@ export async function getUserPermissionConfig( return mergeEnvAllowlist(parsePermissionGroupConfig(groupMembership.config)) } -export async function getPermissionConfig( +/** + * Cache-aware wrapper around `getUserPermissionConfig`. When an + * `ExecutionContext` is provided, the resolved config is memoized on the + * context so repeated checks during a single workflow run share one DB hit. + */ +async function getPermissionConfig( userId: string | undefined, + workspaceId: string | undefined, ctx?: ExecutionContext ): Promise { - if (!userId) { - return null + if (!userId || !workspaceId) { + return mergeEnvAllowlist(null) } if (ctx) { @@ -154,25 +159,26 @@ export async function getPermissionConfig( return ctx.permissionConfig ?? null } - const config = await getUserPermissionConfig(userId) + const config = await getUserPermissionConfig(userId, workspaceId) ctx.permissionConfig = config ctx.permissionConfigLoaded = true return config } - return getUserPermissionConfig(userId) + return getUserPermissionConfig(userId, workspaceId) } export async function validateModelProvider( userId: string | undefined, + workspaceId: string | undefined, model: string, ctx?: ExecutionContext ): Promise { - if (!userId) { + if (!userId || !workspaceId) { return } - const config = await getPermissionConfig(userId, ctx) + const config = await getPermissionConfig(userId, workspaceId, ctx) if (!config || config.allowedModelProviders === null) { return @@ -181,13 +187,19 @@ export async function validateModelProvider( const providerId = getProviderFromModel(model) if (!config.allowedModelProviders.includes(providerId)) { - logger.warn('Model provider blocked by permission group', { userId, model, providerId }) + logger.warn('Model provider blocked by permission group', { + userId, + workspaceId, + model, + providerId, + }) throw new ProviderNotAllowedError(providerId, model) } } export async function validateBlockType( userId: string | undefined, + workspaceId: string | undefined, blockType: string, ctx?: ExecutionContext ): Promise { @@ -195,7 +207,10 @@ export async function validateBlockType( return } - const config = userId ? await getPermissionConfig(userId, ctx) : mergeEnvAllowlist(null) + const config = + userId && workspaceId + ? await getPermissionConfig(userId, workspaceId, ctx) + : mergeEnvAllowlist(null) if (!config || config.allowedIntegrations === null) { return @@ -208,7 +223,7 @@ export async function validateBlockType( blockedByEnv ? 'Integration blocked by env allowlist' : 'Integration blocked by permission group', - { userId, blockType } + { userId, workspaceId, blockType } ) throw new IntegrationNotAllowedError( blockType, @@ -219,70 +234,81 @@ export async function validateBlockType( export async function validateMcpToolsAllowed( userId: string | undefined, + workspaceId: string | undefined, ctx?: ExecutionContext ): Promise { - if (!userId) { + if (!userId || !workspaceId) { return } - const config = await getPermissionConfig(userId, ctx) + const config = await getPermissionConfig(userId, workspaceId, ctx) if (!config) { return } if (config.disableMcpTools) { - logger.warn('MCP tools blocked by permission group', { userId }) + logger.warn('MCP tools blocked by permission group', { userId, workspaceId }) throw new McpToolsNotAllowedError() } } export async function validateCustomToolsAllowed( userId: string | undefined, + workspaceId: string | undefined, ctx?: ExecutionContext ): Promise { - if (!userId) { + if (!userId || !workspaceId) { return } - const config = await getPermissionConfig(userId, ctx) + const config = await getPermissionConfig(userId, workspaceId, ctx) if (!config) { return } if (config.disableCustomTools) { - logger.warn('Custom tools blocked by permission group', { userId }) + logger.warn('Custom tools blocked by permission group', { userId, workspaceId }) throw new CustomToolsNotAllowedError() } } export async function validateSkillsAllowed( userId: string | undefined, + workspaceId: string | undefined, ctx?: ExecutionContext ): Promise { - if (!userId) { + if (!userId || !workspaceId) { return } - const config = await getPermissionConfig(userId, ctx) + const config = await getPermissionConfig(userId, workspaceId, ctx) if (!config) { return } if (config.disableSkills) { - logger.warn('Skills blocked by permission group', { userId }) + logger.warn('Skills blocked by permission group', { userId, workspaceId }) throw new SkillsNotAllowedError() } } /** - * Validates if the user is allowed to send invitations. - * Also checks the global feature flag. + * Validates if the user is allowed to send invitations. Pass one of: + * - `workspaceId` — workspace-scoped invite: block when the user's group in that workspace has + * `disableInvitations`. + * - `organizationId` — organization-level invite (no specific workspace target): block when the + * user has `disableInvitations` set on their group in any organization-owned workspace. This + * mirrors the pre-refactor behavior where `disableInvitations` was an organization-level + * policy. + * - neither — only the global feature flag is checked. */ -export async function validateInvitationsAllowed(userId: string | undefined): Promise { - const { isInvitationsDisabled } = await import('@/lib/core/config/feature-flags') +export async function validateInvitationsAllowed( + userId: string | undefined, + scope: string | { workspaceId?: string; organizationId?: string } = {} +): Promise { if (isInvitationsDisabled) { logger.warn('Invitations blocked by feature flag') throw new InvitationsNotAllowedError() @@ -292,41 +318,154 @@ export async function validateInvitationsAllowed(userId: string | undefined): Pr return } - const config = await getUserPermissionConfig(userId) + const { workspaceId, organizationId } = + typeof scope === 'string' ? { workspaceId: scope, organizationId: undefined } : scope - if (!config) { + if (workspaceId) { + const config = await getUserPermissionConfig(userId, workspaceId) + if (config?.disableInvitations) { + logger.warn('Invitations blocked by permission group', { userId, workspaceId }) + throw new InvitationsNotAllowedError() + } return } - if (config.disableInvitations) { - logger.warn('Invitations blocked by permission group', { userId }) - throw new InvitationsNotAllowedError() + if (organizationId) { + const [row] = await db + .select({ id: permissionGroup.id }) + .from(permissionGroupMember) + .innerJoin(permissionGroup, eq(permissionGroupMember.permissionGroupId, permissionGroup.id)) + .innerJoin(workspace, eq(permissionGroup.workspaceId, workspace.id)) + .where( + and( + eq(permissionGroupMember.userId, userId), + eq(workspace.organizationId, organizationId), + sql`${permissionGroup.config} @> '{"disableInvitations": true}'::jsonb` + ) + ) + .limit(1) + + if (row) { + logger.warn('Invitations blocked by permission group (organization-wide)', { + userId, + organizationId, + permissionGroupId: row.id, + }) + throw new InvitationsNotAllowedError() + } } } /** - * Validates if the user is allowed to enable public API access. - * Also checks the global feature flag. + * Validates if the user is allowed to enable public API access on the given + * workspace. Also checks the global feature flag. When `workspaceId` is + * omitted only the feature-flag check runs (no permission-group gate). */ -export async function validatePublicApiAllowed(userId: string | undefined): Promise { - const { isPublicApiDisabled } = await import('@/lib/core/config/feature-flags') +export async function validatePublicApiAllowed( + userId: string | undefined, + workspaceId?: string +): Promise { if (isPublicApiDisabled) { logger.warn('Public API blocked by feature flag') throw new PublicApiNotAllowedError() } - if (!userId) { + if (!userId || !workspaceId) { return } - const config = await getUserPermissionConfig(userId) + const config = await getUserPermissionConfig(userId, workspaceId) if (!config) { return } if (config.disablePublicApi) { - logger.warn('Public API blocked by permission group', { userId }) + logger.warn('Public API blocked by permission group', { userId, workspaceId }) throw new PublicApiNotAllowedError() } } + +export type ToolKind = 'mcp' | 'custom' | 'skill' + +export interface PermissionAssertion { + userId: string | undefined + workspaceId: string | undefined + model?: string + blockType?: string + toolKind?: ToolKind + ctx?: ExecutionContext +} + +/** + * Unified entry point for workspace-scoped access control. Loads the user's + * permission config for `workspaceId` once and runs every applicable gate + * (model provider, block type, tool kind) against it, throwing the existing + * granular error classes on the first mismatch. + * + * Prefer this over calling the individual `validate*Allowed` helpers when + * gating a shared entry point like `executeTool` or an HTTP proxy, so a single + * callsite covers every future config field. + */ +export async function assertPermissionsAllowed(req: PermissionAssertion): Promise { + const { userId, workspaceId, model, blockType, toolKind, ctx } = req + + if (blockType === 'start_trigger') { + if (!model && !toolKind) { + return + } + } + + const config = + userId && workspaceId + ? await getPermissionConfig(userId, workspaceId, ctx) + : mergeEnvAllowlist(null) + + if (model && config && config.allowedModelProviders !== null) { + const providerId = getProviderFromModel(model) + if (!config.allowedModelProviders.includes(providerId)) { + logger.warn('Model provider blocked by permission group', { + userId, + workspaceId, + model, + providerId, + }) + throw new ProviderNotAllowedError(providerId, model) + } + } + + if (blockType && blockType !== 'start_trigger') { + if (config && config.allowedIntegrations !== null) { + if (!config.allowedIntegrations.includes(blockType.toLowerCase())) { + const envAllowlist = getAllowedIntegrationsFromEnv() + const blockedByEnv = + envAllowlist !== null && !envAllowlist.includes(blockType.toLowerCase()) + logger.warn( + blockedByEnv + ? 'Integration blocked by env allowlist' + : 'Integration blocked by permission group', + { userId, workspaceId, blockType } + ) + throw new IntegrationNotAllowedError( + blockType, + blockedByEnv ? 'blocked by server ALLOWED_INTEGRATIONS policy' : undefined + ) + } + } + } + + if (toolKind && config) { + if (toolKind === 'mcp' && config.disableMcpTools) { + logger.warn('MCP tools blocked by permission group', { userId, workspaceId }) + throw new McpToolsNotAllowedError() + } + if (toolKind === 'custom' && config.disableCustomTools) { + logger.warn('Custom tools blocked by permission group', { userId, workspaceId }) + throw new CustomToolsNotAllowedError() + } + if (toolKind === 'skill' && config.disableSkills) { + logger.warn('Skills blocked by permission group', { userId, workspaceId }) + throw new SkillsNotAllowedError() + } + } +} diff --git a/apps/sim/executor/execution/block-executor.ts b/apps/sim/executor/execution/block-executor.ts index c348aa401fb..170fc805acc 100644 --- a/apps/sim/executor/execution/block-executor.ts +++ b/apps/sim/executor/execution/block-executor.ts @@ -106,7 +106,7 @@ export class BlockExecutor { try { if (!isSentinel && blockType) { - await validateBlockType(ctx.userId, blockType, ctx) + await validateBlockType(ctx.userId, ctx.workspaceId, blockType, ctx) } resolvedInputs = this.resolver.resolveInputs(ctx, node.id, block.config.params, block) diff --git a/apps/sim/executor/handlers/agent/agent-handler.ts b/apps/sim/executor/handlers/agent/agent-handler.ts index fcf43b6844b..31c43b6582d 100644 --- a/apps/sim/executor/handlers/agent/agent-handler.ts +++ b/apps/sim/executor/handlers/agent/agent-handler.ts @@ -64,7 +64,7 @@ export class AgentBlockHandler implements BlockHandler { const responseFormat = parseResponseFormat(filteredInputs.responseFormat) const model = filteredInputs.model || AGENT.DEFAULT_MODEL - await validateModelProvider(ctx.userId, model, ctx) + await validateModelProvider(ctx.userId, ctx.workspaceId, model, ctx) const providerId = getProviderFromModel(model) const formattedTools = await this.formatTools( @@ -76,7 +76,7 @@ export class AgentBlockHandler implements BlockHandler { const skillInputs = filteredInputs.skills ?? [] let skillMetadata: Array<{ name: string; description: string }> = [] if (skillInputs.length > 0 && ctx.workspaceId) { - await validateSkillsAllowed(ctx.userId, ctx) + await validateSkillsAllowed(ctx.userId, ctx.workspaceId, ctx) skillMetadata = await resolveSkillMetadata(skillInputs, ctx.workspaceId) if (skillMetadata.length > 0) { const skillNames = skillMetadata.map((s) => s.name) @@ -125,11 +125,11 @@ export class AgentBlockHandler implements BlockHandler { const hasCustomTools = tools.some((t) => t.type === 'custom-tool') if (hasMcpTools) { - await validateMcpToolsAllowed(ctx.userId, ctx) + await validateMcpToolsAllowed(ctx.userId, ctx.workspaceId, ctx) } if (hasCustomTools) { - await validateCustomToolsAllowed(ctx.userId, ctx) + await validateCustomToolsAllowed(ctx.userId, ctx.workspaceId, ctx) } } @@ -212,7 +212,7 @@ export class AgentBlockHandler implements BlockHandler { otherTools.map(async (tool) => { try { if (tool.type && tool.type !== 'custom-tool') { - await validateBlockType(ctx.userId, tool.type, ctx) + await validateBlockType(ctx.userId, ctx.workspaceId, tool.type, ctx) } if (tool.type === 'custom-tool' && (tool.schema || tool.customToolId)) { return await this.createCustomTool(ctx, tool) diff --git a/apps/sim/executor/handlers/evaluator/evaluator-handler.ts b/apps/sim/executor/handlers/evaluator/evaluator-handler.ts index 95d25ba4656..abb14853ec5 100644 --- a/apps/sim/executor/handlers/evaluator/evaluator-handler.ts +++ b/apps/sim/executor/handlers/evaluator/evaluator-handler.ts @@ -35,7 +35,7 @@ export class EvaluatorBlockHandler implements BlockHandler { bedrockRegion: inputs.bedrockRegion, } - await validateModelProvider(ctx.userId, evaluatorConfig.model, ctx) + await validateModelProvider(ctx.userId, ctx.workspaceId, evaluatorConfig.model, ctx) const providerId = getProviderFromModel(evaluatorConfig.model) diff --git a/apps/sim/executor/handlers/router/router-handler.ts b/apps/sim/executor/handlers/router/router-handler.ts index f4c715d2ced..1f3c7ba5745 100644 --- a/apps/sim/executor/handlers/router/router-handler.ts +++ b/apps/sim/executor/handlers/router/router-handler.ts @@ -71,7 +71,7 @@ export class RouterBlockHandler implements BlockHandler { bedrockRegion: inputs.bedrockRegion, } - await validateModelProvider(ctx.userId, routerConfig.model, ctx) + await validateModelProvider(ctx.userId, ctx.workspaceId, routerConfig.model, ctx) const providerId = getProviderFromModel(routerConfig.model) @@ -201,7 +201,7 @@ export class RouterBlockHandler implements BlockHandler { bedrockRegion: inputs.bedrockRegion, } - await validateModelProvider(ctx.userId, routerConfig.model, ctx) + await validateModelProvider(ctx.userId, ctx.workspaceId, routerConfig.model, ctx) const providerId = getProviderFromModel(routerConfig.model) diff --git a/apps/sim/hooks/queries/workspace.ts b/apps/sim/hooks/queries/workspace.ts index 42142dfb726..16e1ea8d3c5 100644 --- a/apps/sim/hooks/queries/workspace.ts +++ b/apps/sim/hooks/queries/workspace.ts @@ -253,10 +253,28 @@ export interface WorkspaceUser { permissionType: 'admin' | 'write' | 'read' } +/** Viewer context for a workspace permissions response. */ +export interface WorkspacePermissionsViewer { + userId: string + /** + * Mirrors the server's `hasWorkspaceAdminAccess` check: true when the caller is the workspace + * owner, an explicit workspace admin, or an organization owner/admin of an organization-owned + * workspace. Use this instead of scanning `users` for a `permissionType === 'admin'` row. + */ + isAdmin: boolean + /** + * The viewer's effective permission level for this workspace. Resolves to `'admin'` whenever + * `isAdmin` is true (including owner / org-admin paths where no explicit permissions row exists), + * otherwise falls back to the user's explicit `permissions` row (`admin` | `write` | `read`). + */ + permissionType: 'admin' | 'write' | 'read' +} + /** Workspace permissions data containing all users and their access levels. */ export interface WorkspacePermissions { users: WorkspaceUser[] total: number + viewer?: WorkspacePermissionsViewer } async function fetchWorkspacePermissions( diff --git a/apps/sim/hooks/use-permission-config.ts b/apps/sim/hooks/use-permission-config.ts index f52b8aae18c..c61a8045e75 100644 --- a/apps/sim/hooks/use-permission-config.ts +++ b/apps/sim/hooks/use-permission-config.ts @@ -2,14 +2,13 @@ import { useMemo } from 'react' import { useQuery } from '@tanstack/react-query' +import { useParams } from 'next/navigation' import { getEnv, isTruthy } from '@/lib/core/config/env' -import { isAccessControlEnabled, isHosted } from '@/lib/core/config/feature-flags' import { DEFAULT_PERMISSION_GROUP_CONFIG, type PermissionGroupConfig, } from '@/lib/permission-groups/types' import { useUserPermissionConfig } from '@/ee/access-control/hooks/permission-groups' -import { useOrganizations } from '@/hooks/queries/organization' export interface PermissionConfigResult { config: PermissionGroupConfig @@ -50,29 +49,24 @@ function intersectAllowlists(a: string[] | null, b: string[] | null): string[] | } export function usePermissionConfig(): PermissionConfigResult { - const accessControlDisabled = !isHosted && !isAccessControlEnabled - const { data: organizationsData } = useOrganizations() - const activeOrganization = organizationsData?.activeOrganization + const params = useParams() + const workspaceId = typeof params?.workspaceId === 'string' ? params.workspaceId : undefined - const { data: permissionData, isLoading: isPermissionLoading } = useUserPermissionConfig( - activeOrganization?.id - ) + const { data: permissionData, isLoading: isPermissionLoading } = + useUserPermissionConfig(workspaceId) const { data: envAllowlistData, isLoading: isEnvAllowlistLoading } = useAllowedIntegrationsFromEnv() const isLoading = isPermissionLoading || isEnvAllowlistLoading const config = useMemo(() => { - if (accessControlDisabled) { - return DEFAULT_PERMISSION_GROUP_CONFIG - } if (!permissionData?.config) { return DEFAULT_PERMISSION_GROUP_CONFIG } return permissionData.config - }, [permissionData, accessControlDisabled]) + }, [permissionData]) - const isInPermissionGroup = !accessControlDisabled && !!permissionData?.permissionGroupId + const isInPermissionGroup = !!permissionData?.permissionGroupId const mergedAllowedIntegrations = useMemo(() => { const envAllowlist = envAllowlistData?.allowedIntegrations ?? null diff --git a/apps/sim/hooks/use-user-permissions.ts b/apps/sim/hooks/use-user-permissions.ts index c30a80bc1c2..d17e4a5fd04 100644 --- a/apps/sim/hooks/use-user-permissions.ts +++ b/apps/sim/hooks/use-user-permissions.ts @@ -48,12 +48,28 @@ export function useUserPermissions( } } - // Find current user in workspace permissions (case-insensitive) + /** + * Prefer the server-resolved `viewer.permissionType` — it already accounts for workspace + * owners and organization owners/admins who have no explicit `permissions` row but are + * effectively admins. Falls back to scanning `users` only if the server response predates + * the viewer field (rolling deploy). + */ + const viewerPerms = workspacePermissions?.viewer?.permissionType + if (viewerPerms) { + return { + canRead: true, + canEdit: viewerPerms === 'write' || viewerPerms === 'admin', + canAdmin: viewerPerms === 'admin', + userPermissions: viewerPerms, + isLoading: false, + error: permissionsError, + } + } + const currentUser = workspacePermissions?.users?.find( (user) => user.email.toLowerCase() === sessionEmail.toLowerCase() ) - // If user not found in workspace, they have no permissions if (!currentUser) { logger.warn('User not found in workspace permissions', { userEmail: sessionEmail, @@ -72,14 +88,11 @@ export function useUserPermissions( } const userPerms = currentUser.permissionType || 'read' - - // Core permission checks const canAdmin = userPerms === 'admin' const canEdit = userPerms === 'write' || userPerms === 'admin' - const canRead = true // If user is found in workspace permissions, they have read access return { - canRead, + canRead: true, canEdit, canAdmin, userPermissions: userPerms, diff --git a/apps/sim/lib/auth/auth.ts b/apps/sim/lib/auth/auth.ts index ecdd27fdef7..f770920fc19 100644 --- a/apps/sim/lib/auth/auth.ts +++ b/apps/sim/lib/auth/auth.ts @@ -188,6 +188,14 @@ export const auth = betterAuth({ `You are the owner of ${check.organizationName ?? 'an active paid organization'}. Transfer ownership before deleting your account.` ) } + + const { reassignBilledAccountForUser } = await import('@/lib/workspaces/utils') + const { unresolved } = await reassignBilledAccountForUser(deletingUser.id) + if (unresolved.length > 0) { + throw new Error( + `Your account is the billing account for ${unresolved.length} workspace${unresolved.length === 1 ? '' : 's'} with no other admin to take it over. Add another admin to ${unresolved.length === 1 ? 'that workspace' : 'those workspaces'} or delete ${unresolved.length === 1 ? 'it' : 'them'} before deleting your account.` + ) + } }, }, }, diff --git a/apps/sim/lib/billing/core/subscription.ts b/apps/sim/lib/billing/core/subscription.ts index 0a4b1766b8b..75dfd706291 100644 --- a/apps/sim/lib/billing/core/subscription.ts +++ b/apps/sim/lib/billing/core/subscription.ts @@ -471,22 +471,30 @@ export async function hasSSOAccess(userId: string): Promise { } /** - * Check if user has access to Access Control (Permission Groups) feature - * Returns true if: - * - ACCESS_CONTROL_ENABLED env var is set (self-hosted override), OR - * - User is admin/owner of an enterprise organization - * - * In non-production environments, returns true for convenience. + * Check whether a workspace is entitled to the Access Control (Permission Groups) + * feature. Entitlement follows the workspace's `billedAccountUserId`: + * - self-hosted override honored via ACCESS_CONTROL_ENABLED, OR + * - billing disabled, OR + * - the workspace belongs to an enterprise-plan organization (org-mode), OR + * - the billed user has an individual enterprise subscription (personal workspace). */ -export async function hasAccessControlAccess(userId: string): Promise { +export async function isWorkspaceOnEnterprisePlan(workspaceId: string): Promise { try { - if (isAccessControlEnabled && !isHosted) { - return true + if (!isBillingEnabled) return true + if (isAccessControlEnabled && !isHosted) return true + + const { getWorkspaceWithOwner } = await import('@/lib/workspaces/permissions/utils') + const ws = await getWorkspaceWithOwner(workspaceId, { includeArchived: true }) + if (!ws) return false + + if (ws.organizationId) { + return isOrganizationOnEnterprisePlan(ws.organizationId) } - return isEnterpriseOrgAdminOrOwner(userId) + const billedSub = await getHighestPrioritySubscription(ws.billedAccountUserId) + return !!billedSub && checkEnterprisePlan(billedSub) } catch (error) { - logger.error('Error checking access control access', { error, userId }) + logger.error('Error checking workspace enterprise plan status', { error, workspaceId }) return false } } diff --git a/apps/sim/lib/billing/index.ts b/apps/sim/lib/billing/index.ts index b4260c82952..8c01b7928c6 100644 --- a/apps/sim/lib/billing/index.ts +++ b/apps/sim/lib/billing/index.ts @@ -9,7 +9,6 @@ export * from '@/lib/billing/core/organization' export * from '@/lib/billing/core/subscription' export { getHighestPrioritySubscription as getActiveSubscription, - hasAccessControlAccess, hasCredentialSetsAccess, hasPaidSubscription, hasSSOAccess, @@ -20,6 +19,7 @@ export { isProPlan as hasProPlan, isTeamOrgAdminOrOwner, isTeamPlan as hasTeamPlan, + isWorkspaceOnEnterprisePlan, sendPlanWelcomeEmail, } from '@/lib/billing/core/subscription' export * from '@/lib/billing/core/usage' diff --git a/apps/sim/lib/billing/organizations/membership.ts b/apps/sim/lib/billing/organizations/membership.ts index f4cb9d3e135..ff96c5bd9f5 100644 --- a/apps/sim/lib/billing/organizations/membership.ts +++ b/apps/sim/lib/billing/organizations/membership.ts @@ -942,6 +942,31 @@ export async function transferOrganizationOwnership( result.workspacesReassigned = ownerUpdate.length + const reassignedWorkspaceIds = Array.from( + new Set([...billedUpdate.map((w) => w.id), ...ownerUpdate.map((w) => w.id)]) + ) + + if (reassignedWorkspaceIds.length > 0) { + const now = new Date() + await tx + .insert(permissions) + .values( + reassignedWorkspaceIds.map((workspaceId) => ({ + id: generateId(), + userId: newOwnerUserId, + entityType: 'workspace' as const, + entityId: workspaceId, + permissionType: 'admin' as const, + createdAt: now, + updatedAt: now, + })) + ) + .onConflictDoUpdate({ + target: [permissions.userId, permissions.entityType, permissions.entityId], + set: { permissionType: 'admin', updatedAt: now }, + }) + } + const [oldStats] = await tx .select({ billedOverageThisPeriod: userStats.billedOverageThisPeriod, diff --git a/apps/sim/lib/copilot/chat/payload.ts b/apps/sim/lib/copilot/chat/payload.ts index 6f6f297dda7..089ed11705d 100644 --- a/apps/sim/lib/copilot/chat/payload.ts +++ b/apps/sim/lib/copilot/chat/payload.ts @@ -59,13 +59,19 @@ interface BuildIntegrationToolSchemasOptions { * Build deferred integration tool schemas from the Sim tool registry. * Shared by the interactive chat payload builder and the non-interactive * block execution route so both paths send the same tool definitions to Go. + * + * When `workspaceId` is provided the user's workspace permission config is + * loaded once and used to skip any tool whose owning block is not in the + * workspace's `allowedIntegrations` allowlist. The resulting list is cached + * per `(userId, workspaceId, surface)` key so copilot turns reuse the filter. */ export async function buildIntegrationToolSchemas( userId: string, messageId?: string, - options: BuildIntegrationToolSchemasOptions = { schemaSurface: 'copilot' } + options: BuildIntegrationToolSchemasOptions = { schemaSurface: 'copilot' }, + workspaceId?: string ): Promise { - const cacheKey = `${userId}:${options.schemaSurface ?? 'copilot'}` + const cacheKey = `${userId}:${workspaceId ?? ''}:${options.schemaSurface ?? 'copilot'}` const now = Date.now() const cached = toolSchemaCache.get(cacheKey) if (cached?.value && cached.expiresAt > now) { @@ -94,12 +100,49 @@ export async function buildIntegrationToolSchemas( }) } + let allowedIntegrations: Set | null = null + let toolIdToBlockType: Map | null = null + if (workspaceId) { + try { + const [{ getUserPermissionConfig }, { registry: blockRegistry }] = await Promise.all([ + import('@/ee/access-control/utils/permission-check'), + import('@/blocks/registry'), + ]) + const permissionConfig = await getUserPermissionConfig(userId, workspaceId) + if (permissionConfig?.allowedIntegrations) { + allowedIntegrations = new Set( + permissionConfig.allowedIntegrations.map((i) => i.toLowerCase()) + ) + toolIdToBlockType = new Map() + for (const [blockType, blockConfig] of Object.entries(blockRegistry)) { + const access = (blockConfig as { tools?: { access?: string[] } }).tools?.access + if (!access) continue + for (const toolId of access) { + toolIdToBlockType.set(stripVersionSuffix(toolId), blockType.toLowerCase()) + } + } + } + } catch (error) { + reqLogger.warn('Failed to load permission config for tool schema filter', { + userId, + workspaceId, + error: toError(error).message, + }) + } + } + for (const [toolId, toolConfig] of Object.entries(latestTools)) { try { + const strippedName = stripVersionSuffix(toolId) + if (allowedIntegrations && toolIdToBlockType) { + const owningBlock = toolIdToBlockType.get(strippedName) + if (owningBlock && !allowedIntegrations.has(owningBlock)) { + continue + } + } const userSchema = createUserToolSchema(toolConfig, { surface: options.schemaSurface ?? 'copilot', }) - const strippedName = stripVersionSuffix(toolId) const catalogEntry = getToolEntry(strippedName) integrationTools.push({ name: strippedName, @@ -235,9 +278,12 @@ export async function buildCopilotRequestPayload( const payloadLogger = logger.withMetadata({ messageId: userMessageId }) if (effectiveMode === 'build') { - integrationTools = await buildIntegrationToolSchemas(userId, userMessageId, { - schemaSurface: 'copilot', - }) + integrationTools = await buildIntegrationToolSchemas( + userId, + userMessageId, + { schemaSurface: 'copilot' }, + params.workspaceId + ) // Discover MCP tools from workspace servers and include as deferred tools if (params.workspaceId) { diff --git a/apps/sim/lib/copilot/chat/process-contents.ts b/apps/sim/lib/copilot/chat/process-contents.ts index 89fc036c243..f62109e9202 100644 --- a/apps/sim/lib/copilot/chat/process-contents.ts +++ b/apps/sim/lib/copilot/chat/process-contents.ts @@ -85,7 +85,8 @@ export async function processContextsServer( return await processBlockMetadata( ctx.blockIds[0], ctx.label ? `@${ctx.label}` : '@', - userId + userId, + currentWorkspaceId ) } if (ctx.kind === 'templates' && ctx.templateId) { @@ -446,10 +447,12 @@ async function processKnowledgeFromDb( async function processBlockMetadata( blockId: string, tag: string, - userId?: string + userId?: string, + workspaceId?: string ): Promise { try { - const permissionConfig = userId ? await getUserPermissionConfig(userId) : null + const permissionConfig = + userId && workspaceId ? await getUserPermissionConfig(userId, workspaceId) : null const allowedIntegrations = permissionConfig?.allowedIntegrations ?? getAllowedIntegrationsFromEnv() if (allowedIntegrations != null && !allowedIntegrations.includes(blockId.toLowerCase())) { diff --git a/apps/sim/lib/copilot/tools/server/blocks/get-blocks-metadata-tool.ts b/apps/sim/lib/copilot/tools/server/blocks/get-blocks-metadata-tool.ts index 5b94a4a8966..f9d0f159e8c 100644 --- a/apps/sim/lib/copilot/tools/server/blocks/get-blocks-metadata-tool.ts +++ b/apps/sim/lib/copilot/tools/server/blocks/get-blocks-metadata-tool.ts @@ -113,12 +113,15 @@ export const getBlocksMetadataServerTool: BaseServerTool< outputSchema: GetBlocksMetadataResultSchema, async execute( { blockIds }: z.infer, - context?: { userId: string } + context?: { userId: string; workspaceId?: string } ): Promise> { const logger = createLogger('GetBlocksMetadataServerTool') logger.debug('Executing get_blocks_metadata', { count: blockIds?.length }) - const permissionConfig = context?.userId ? await getUserPermissionConfig(context.userId) : null + const permissionConfig = + context?.userId && context?.workspaceId + ? await getUserPermissionConfig(context.userId, context.workspaceId) + : null const allowedIntegrations = permissionConfig?.allowedIntegrations ?? getAllowedIntegrationsFromEnv() diff --git a/apps/sim/lib/copilot/tools/server/blocks/get-trigger-blocks.ts b/apps/sim/lib/copilot/tools/server/blocks/get-trigger-blocks.ts index df77ad6f7a0..c8ac8c37372 100644 --- a/apps/sim/lib/copilot/tools/server/blocks/get-trigger-blocks.ts +++ b/apps/sim/lib/copilot/tools/server/blocks/get-trigger-blocks.ts @@ -18,11 +18,14 @@ export const getTriggerBlocksServerTool: BaseServerTool< name: 'get_trigger_blocks', inputSchema: GetTriggerBlocksInput, outputSchema: GetTriggerBlocksResult, - async execute(_args: unknown, context?: { userId: string }) { + async execute(_args: unknown, context?: { userId: string; workspaceId?: string }) { const logger = createLogger('GetTriggerBlocksServerTool') logger.debug('Executing get_trigger_blocks') - const permissionConfig = context?.userId ? await getUserPermissionConfig(context.userId) : null + const permissionConfig = + context?.userId && context?.workspaceId + ? await getUserPermissionConfig(context.userId, context.workspaceId) + : null const allowedIntegrations = permissionConfig?.allowedIntegrations ?? getAllowedIntegrationsFromEnv() diff --git a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/index.ts b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/index.ts index 77b0c886b1d..974388e9ece 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/index.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/index.ts @@ -119,8 +119,10 @@ export const editWorkflowServerTool: BaseServerTool workflowState = fromDb.workflowState } - // Get permission config for the user - const permissionConfig = context?.userId ? await getUserPermissionConfig(context.userId) : null + const permissionConfig = + context?.userId && workspaceId + ? await getUserPermissionConfig(context.userId, workspaceId) + : null // Pre-validate credential and apiKey inputs before applying operations // This filters out invalid credentials and apiKeys for hosted models diff --git a/apps/sim/lib/invitations/core.ts b/apps/sim/lib/invitations/core.ts index f472aa45032..2565ac1b522 100644 --- a/apps/sim/lib/invitations/core.ts +++ b/apps/sim/lib/invitations/core.ts @@ -5,8 +5,6 @@ import { invitation, invitationWorkspaceGrant, organization, - permissionGroup, - permissionGroupMember, permissions, user, workspace, @@ -16,10 +14,10 @@ import { createLogger } from '@sim/logger' import { generateId } from '@sim/utils/id' import { and, eq, inArray, lte } from 'drizzle-orm' import { setActiveOrganizationForCurrentSession } from '@/lib/auth/active-organization' -import { hasAccessControlAccess } from '@/lib/billing' import { syncUsageLimitsFromSubscription } from '@/lib/billing/core/usage' import { ensureUserInOrganization } from '@/lib/billing/organizations/membership' import { syncWorkspaceEnvCredentials } from '@/lib/credentials/environment' +import { applyWorkspaceAutoAddGroup } from '@/lib/permission-groups/auto-add' const logger = createLogger('InvitationCore') @@ -290,49 +288,6 @@ export async function acceptInvitation( .set({ status: 'accepted', updatedAt: new Date() }) .where(eq(invitation.id, inv.id)) - if (inv.organizationId) { - try { - const hasAccessControl = await hasAccessControlAccess(input.userId) - if (hasAccessControl) { - const [autoAddGroup] = await tx - .select({ id: permissionGroup.id, name: permissionGroup.name }) - .from(permissionGroup) - .where( - and( - eq(permissionGroup.organizationId, inv.organizationId), - eq(permissionGroup.autoAddNewMembers, true) - ) - ) - .limit(1) - - if (autoAddGroup) { - const [existingMember] = await tx - .select({ id: permissionGroupMember.id }) - .from(permissionGroupMember) - .where(eq(permissionGroupMember.userId, input.userId)) - .limit(1) - - if (!existingMember) { - await tx.insert(permissionGroupMember).values({ - id: generateId(), - permissionGroupId: autoAddGroup.id, - userId: input.userId, - assignedBy: null, - assignedAt: new Date(), - }) - } - } - } - } catch (groupError) { - logger.error('Failed to auto-assign user to permission group', { - userId: input.userId, - organizationId: inv.organizationId, - invitationId: inv.id, - error: groupError, - }) - } - } - for (const grant of inv.grants) { const [existingPermission] = await tx .select({ id: permissions.id, permissionType: permissions.permissionType }) @@ -370,6 +325,8 @@ export async function acceptInvitation( }) } + await applyWorkspaceAutoAddGroup(tx, grant.workspaceId, input.userId) + acceptedWorkspaceIds.push(grant.workspaceId) } }) diff --git a/apps/sim/lib/mothership/inbox/executor.ts b/apps/sim/lib/mothership/inbox/executor.ts index b46ea43a67c..b738a6a37ae 100644 --- a/apps/sim/lib/mothership/inbox/executor.ts +++ b/apps/sim/lib/mothership/inbox/executor.ts @@ -166,7 +166,7 @@ export async function executeInboxTask(taskId: string): Promise { await Promise.all([ fetchAttachments(), generateWorkspaceContext(ws.id, userId), - buildIntegrationToolSchemas(userId), + buildIntegrationToolSchemas(userId, undefined, undefined, ws.id), getUserEntityPermissions(userId, 'workspace', ws.id).catch(() => null), ]) const { attachments, fileAttachments, storedAttachments } = attachmentResult diff --git a/apps/sim/lib/permission-groups/auto-add.ts b/apps/sim/lib/permission-groups/auto-add.ts new file mode 100644 index 00000000000..8eae5876680 --- /dev/null +++ b/apps/sim/lib/permission-groups/auto-add.ts @@ -0,0 +1,69 @@ +import type { db } from '@sim/db' +import { permissionGroup, permissionGroupMember } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { generateId } from '@sim/utils/id' +import { and, eq } from 'drizzle-orm' +import { isWorkspaceOnEnterprisePlan } from '@/lib/billing' + +const logger = createLogger('PermissionGroupsAutoAdd') + +type DbClient = typeof db +type TransactionClient = Parameters[0]>[0] +type Client = DbClient | TransactionClient + +/** + * Add `userId` to the workspace's `autoAddNewMembers` permission group if one + * exists, the workspace is entitled to access control, and the user is not + * already a member of a group in that workspace. Safe to call unconditionally + * on any workspace-permission grant; no-ops when access control isn't + * available or no auto-add group is configured. + */ +export async function applyWorkspaceAutoAddGroup( + client: Client, + workspaceId: string, + userId: string +): Promise { + try { + const entitled = await isWorkspaceOnEnterprisePlan(workspaceId) + if (!entitled) return + + const [autoAddGroup] = await client + .select({ id: permissionGroup.id }) + .from(permissionGroup) + .where( + and( + eq(permissionGroup.workspaceId, workspaceId), + eq(permissionGroup.autoAddNewMembers, true) + ) + ) + .limit(1) + + if (!autoAddGroup) return + + const [existingMembership] = await client + .select({ id: permissionGroupMember.id }) + .from(permissionGroupMember) + .innerJoin(permissionGroup, eq(permissionGroupMember.permissionGroupId, permissionGroup.id)) + .where( + and(eq(permissionGroupMember.userId, userId), eq(permissionGroup.workspaceId, workspaceId)) + ) + .limit(1) + + if (existingMembership) return + + await client.insert(permissionGroupMember).values({ + id: generateId(), + permissionGroupId: autoAddGroup.id, + workspaceId, + userId, + assignedBy: null, + assignedAt: new Date(), + }) + } catch (error) { + logger.error('Failed to auto-assign user to workspace permission group', { + userId, + workspaceId, + error, + }) + } +} diff --git a/apps/sim/lib/permission-groups/types.ts b/apps/sim/lib/permission-groups/types.ts index c76ad7d1a8f..3630691217c 100644 --- a/apps/sim/lib/permission-groups/types.ts +++ b/apps/sim/lib/permission-groups/types.ts @@ -1,7 +1,37 @@ +import { z } from 'zod' + +export const PERMISSION_GROUP_MEMBER_CONSTRAINTS = { + groupUser: 'permission_group_member_group_user_unique', + workspaceUser: 'permission_group_member_workspace_user_unique', +} as const + +export const permissionGroupConfigSchema = z.object({ + allowedIntegrations: z.array(z.string()).nullable().optional(), + allowedModelProviders: z.array(z.string()).nullable().optional(), + hideTraceSpans: z.boolean().optional(), + hideKnowledgeBaseTab: z.boolean().optional(), + hideTablesTab: z.boolean().optional(), + hideCopilot: z.boolean().optional(), + hideIntegrationsTab: z.boolean().optional(), + hideSecretsTab: z.boolean().optional(), + hideApiKeysTab: z.boolean().optional(), + hideInboxTab: z.boolean().optional(), + hideFilesTab: z.boolean().optional(), + disableMcpTools: z.boolean().optional(), + disableCustomTools: z.boolean().optional(), + disableSkills: z.boolean().optional(), + disableInvitations: z.boolean().optional(), + disablePublicApi: z.boolean().optional(), + hideDeployApi: z.boolean().optional(), + hideDeployMcp: z.boolean().optional(), + hideDeployA2a: z.boolean().optional(), + hideDeployChatbot: z.boolean().optional(), + hideDeployTemplate: z.boolean().optional(), +}) + export interface PermissionGroupConfig { allowedIntegrations: string[] | null allowedModelProviders: string[] | null - // Platform Configuration hideTraceSpans: boolean hideKnowledgeBaseTab: boolean hideTablesTab: boolean @@ -10,15 +40,12 @@ export interface PermissionGroupConfig { hideSecretsTab: boolean hideApiKeysTab: boolean hideInboxTab: boolean - hideEnvironmentTab: boolean hideFilesTab: boolean disableMcpTools: boolean disableCustomTools: boolean disableSkills: boolean - hideTemplates: boolean disableInvitations: boolean disablePublicApi: boolean - // Deploy Modal Tabs hideDeployApi: boolean hideDeployMcp: boolean hideDeployA2a: boolean @@ -37,12 +64,10 @@ export const DEFAULT_PERMISSION_GROUP_CONFIG: PermissionGroupConfig = { hideSecretsTab: false, hideApiKeysTab: false, hideInboxTab: false, - hideEnvironmentTab: false, hideFilesTab: false, disableMcpTools: false, disableCustomTools: false, disableSkills: false, - hideTemplates: false, disableInvitations: false, disablePublicApi: false, hideDeployApi: false, @@ -68,20 +93,13 @@ export function parsePermissionGroupConfig(config: unknown): PermissionGroupConf hideTablesTab: typeof c.hideTablesTab === 'boolean' ? c.hideTablesTab : false, hideCopilot: typeof c.hideCopilot === 'boolean' ? c.hideCopilot : false, hideIntegrationsTab: typeof c.hideIntegrationsTab === 'boolean' ? c.hideIntegrationsTab : false, - hideSecretsTab: - typeof c.hideSecretsTab === 'boolean' - ? c.hideSecretsTab - : typeof c.hideEnvironmentTab === 'boolean' - ? c.hideEnvironmentTab - : false, + hideSecretsTab: typeof c.hideSecretsTab === 'boolean' ? c.hideSecretsTab : false, hideApiKeysTab: typeof c.hideApiKeysTab === 'boolean' ? c.hideApiKeysTab : false, hideInboxTab: typeof c.hideInboxTab === 'boolean' ? c.hideInboxTab : false, - hideEnvironmentTab: typeof c.hideEnvironmentTab === 'boolean' ? c.hideEnvironmentTab : false, hideFilesTab: typeof c.hideFilesTab === 'boolean' ? c.hideFilesTab : false, disableMcpTools: typeof c.disableMcpTools === 'boolean' ? c.disableMcpTools : false, disableCustomTools: typeof c.disableCustomTools === 'boolean' ? c.disableCustomTools : false, disableSkills: typeof c.disableSkills === 'boolean' ? c.disableSkills : false, - hideTemplates: typeof c.hideTemplates === 'boolean' ? c.hideTemplates : false, disableInvitations: typeof c.disableInvitations === 'boolean' ? c.disableInvitations : false, disablePublicApi: typeof c.disablePublicApi === 'boolean' ? c.disablePublicApi : false, hideDeployApi: typeof c.hideDeployApi === 'boolean' ? c.hideDeployApi : false, diff --git a/apps/sim/lib/workspaces/permissions/utils.test.ts b/apps/sim/lib/workspaces/permissions/utils.test.ts index ed9d3fa528e..4833ef5cebc 100644 --- a/apps/sim/lib/workspaces/permissions/utils.test.ts +++ b/apps/sim/lib/workspaces/permissions/utils.test.ts @@ -208,6 +208,7 @@ describe('Permission Utils', () => { userId: 'user1', email: 'alice@example.com', name: 'Alice Smith', + image: 'https://example.com/alice.png', permissionType: 'admin' as PermissionType, }, ] @@ -222,6 +223,7 @@ describe('Permission Utils', () => { userId: 'user1', email: 'alice@example.com', name: 'Alice Smith', + image: 'https://example.com/alice.png', permissionType: 'admin', }, ]) diff --git a/apps/sim/lib/workspaces/permissions/utils.ts b/apps/sim/lib/workspaces/permissions/utils.ts index 105e6c6a17a..0c59174b26c 100644 --- a/apps/sim/lib/workspaces/permissions/utils.ts +++ b/apps/sim/lib/workspaces/permissions/utils.ts @@ -238,6 +238,7 @@ export async function getUsersWithPermissions(workspaceId: string): Promise< userId: string email: string name: string + image: string | null permissionType: PermissionType }> > { @@ -246,6 +247,7 @@ export async function getUsersWithPermissions(workspaceId: string): Promise< userId: user.id, email: user.email, name: user.name, + image: user.image, permissionType: permissions.permissionType, }) .from(permissions) @@ -264,6 +266,7 @@ export async function getUsersWithPermissions(workspaceId: string): Promise< userId: row.userId, email: row.email, name: row.name, + image: row.image ?? null, permissionType: row.permissionType, })) } diff --git a/apps/sim/lib/workspaces/utils.ts b/apps/sim/lib/workspaces/utils.ts index c485eb1e6c4..ff84484a7d0 100644 --- a/apps/sim/lib/workspaces/utils.ts +++ b/apps/sim/lib/workspaces/utils.ts @@ -1,6 +1,9 @@ import { db } from '@sim/db' import { permissions, workspace as workspaceTable } from '@sim/db/schema' -import { and, desc, eq, isNull, sql } from 'drizzle-orm' +import { createLogger } from '@sim/logger' +import { and, desc, eq, isNull, ne, sql } from 'drizzle-orm' + +const logger = createLogger('WorkspaceUtils') interface WorkspaceBillingSettings { billedAccountUserId: string | null @@ -73,3 +76,82 @@ export async function listUserWorkspaces(userId: string, scope: WorkspaceScope = role: row.ownerId === userId ? 'owner' : row.permissionType, })) } + +export interface ReassignBilledAccountResult { + reassigned: Array<{ workspaceId: string; newBilledAccountUserId: string }> + unresolved: string[] +} + +/** + * Reassigns `billedAccountUserId` on every workspace that points to `departingUserId` to + * another eligible user, so the user can be deleted without violating the `workspace.billed_account_user_id` + * foreign key (`ON DELETE NO ACTION`). + * + * Preference order for the replacement: + * 1. The workspace owner (if different from the departing user) + * 2. Any existing workspace admin + * + * Returns the list of workspaces that could not be reassigned (no owner + no admin). Callers should + * block user deletion when `unresolved.length > 0` so we never leave an orphaned billing reference. + */ +export async function reassignBilledAccountForUser( + departingUserId: string +): Promise { + const billedWorkspaces = await db + .select({ + id: workspaceTable.id, + ownerId: workspaceTable.ownerId, + }) + .from(workspaceTable) + .where(eq(workspaceTable.billedAccountUserId, departingUserId)) + + if (billedWorkspaces.length === 0) { + return { reassigned: [], unresolved: [] } + } + + const reassigned: ReassignBilledAccountResult['reassigned'] = [] + const unresolved: string[] = [] + + for (const ws of billedWorkspaces) { + let replacement: string | null = ws.ownerId !== departingUserId ? ws.ownerId : null + + if (!replacement) { + const [admin] = await db + .select({ userId: permissions.userId }) + .from(permissions) + .where( + and( + eq(permissions.entityType, 'workspace'), + eq(permissions.entityId, ws.id), + eq(permissions.permissionType, 'admin'), + ne(permissions.userId, departingUserId) + ) + ) + .limit(1) + + replacement = admin?.userId ?? null + } + + if (!replacement) { + unresolved.push(ws.id) + continue + } + + await db + .update(workspaceTable) + .set({ billedAccountUserId: replacement, updatedAt: new Date() }) + .where(eq(workspaceTable.id, ws.id)) + + reassigned.push({ workspaceId: ws.id, newBilledAccountUserId: replacement }) + } + + if (reassigned.length > 0) { + logger.info('Reassigned workspace billed account for departing user', { + departingUserId, + reassignedCount: reassigned.length, + unresolvedCount: unresolved.length, + }) + } + + return { reassigned, unresolved } +} diff --git a/apps/sim/tools/index.test.ts b/apps/sim/tools/index.test.ts index 3ac01469cd6..2e47e8c0ecc 100644 --- a/apps/sim/tools/index.test.ts +++ b/apps/sim/tools/index.test.ts @@ -78,6 +78,25 @@ vi.mock('@/lib/auth/internal', () => ({ generateInternalToken: (...args: unknown[]) => mockGenerateInternalToken(...args), })) +vi.mock('@/ee/access-control/utils/permission-check', () => ({ + assertPermissionsAllowed: vi.fn().mockResolvedValue(undefined), + validateBlockType: vi.fn().mockResolvedValue(undefined), + validateMcpToolsAllowed: vi.fn().mockResolvedValue(undefined), + validateCustomToolsAllowed: vi.fn().mockResolvedValue(undefined), + validateSkillsAllowed: vi.fn().mockResolvedValue(undefined), + validateModelProvider: vi.fn().mockResolvedValue(undefined), + validateInvitationsAllowed: vi.fn().mockResolvedValue(undefined), + validatePublicApiAllowed: vi.fn().mockResolvedValue(undefined), + getUserPermissionConfig: vi.fn().mockResolvedValue(null), + ProviderNotAllowedError: class ProviderNotAllowedError extends Error {}, + IntegrationNotAllowedError: class IntegrationNotAllowedError extends Error {}, + McpToolsNotAllowedError: class McpToolsNotAllowedError extends Error {}, + CustomToolsNotAllowedError: class CustomToolsNotAllowedError extends Error {}, + SkillsNotAllowedError: class SkillsNotAllowedError extends Error {}, + InvitationsNotAllowedError: class InvitationsNotAllowedError extends Error {}, + PublicApiNotAllowedError: class PublicApiNotAllowedError extends Error {}, +})) + vi.mock('@/lib/billing/core/usage-log', () => ({})) vi.mock('@/lib/core/security/input-validation.server', () => inputValidationMock) diff --git a/apps/sim/tools/index.ts b/apps/sim/tools/index.ts index 8e85c2de38f..3d173c6b4ca 100644 --- a/apps/sim/tools/index.ts +++ b/apps/sim/tools/index.ts @@ -17,6 +17,7 @@ import { isUserFile } from '@/lib/core/utils/user-file' import { SIM_VIA_HEADER, serializeCallChain } from '@/lib/execution/call-chain' import { parseMcpToolId } from '@/lib/mcp/utils' import { resolveWorkspaceFileReference } from '@/lib/uploads/contexts/workspace/workspace-file-manager' +import { assertPermissionsAllowed } from '@/ee/access-control/utils/permission-check' import { isCustomTool, isMcpTool } from '@/executor/constants' import { resolveSkillContent } from '@/executor/handlers/agent/skills-resolver' import type { ExecutionContext, UserFile } from '@/executor/types' @@ -766,7 +767,24 @@ export async function executeTool( const scope = resolveToolScope(params, executionContext) - // Handle load_skill tool for agent skills progressive disclosure + const toolKind: 'skill' | 'custom' | 'mcp' | undefined = + normalizedToolId === 'load_skill' + ? 'skill' + : isCustomTool(normalizedToolId) + ? 'custom' + : isMcpTool(normalizedToolId) + ? 'mcp' + : undefined + + if (toolKind && scope.userId && scope.workspaceId) { + await assertPermissionsAllowed({ + userId: scope.userId, + workspaceId: scope.workspaceId, + toolKind, + ctx: executionContext, + }) + } + if (normalizedToolId === 'load_skill') { const skillName = params.skill_name if (!skillName || !scope.workspaceId) { @@ -790,7 +808,6 @@ export async function executeTool( } } - // If it's a custom tool, use the async version if (isCustomTool(normalizedToolId)) { tool = await toolsUtilsServer.getToolAsync(normalizedToolId, { workflowId: scope.workflowId, diff --git a/packages/db/migrations/0194_careless_pete_wisdom.sql b/packages/db/migrations/0194_careless_pete_wisdom.sql new file mode 100644 index 00000000000..e1ab6769c36 --- /dev/null +++ b/packages/db/migrations/0194_careless_pete_wisdom.sql @@ -0,0 +1,128 @@ +-- Rebase permission groups from organization-scoped to workspace-scoped. +-- Each existing org-scoped permission_group is cloned onto every workspace +-- owned by that org; members are copied to each clone only if the user has +-- workspace-level permissions on the target workspace. The +-- permission_group_member table also gains a denormalized workspace_id column +-- so the database can enforce the "one group per user per workspace" +-- invariant with a composite unique index. + +-- 0. Backfill workspace -> organization links for grandfathered workspaces whose +-- billed account user is the sole owner of exactly one organization. This is a +-- best-effort reconciliation: migration 0192 defaulted every pre-existing +-- workspace to `grandfathered_shared` with `organization_id = NULL`, but many +-- of those workspaces belong to users who own a single org. Without this link, +-- the permission-group clone step below would drop all access control data for +-- those workspaces. We only attach when ownership is unambiguous (user owns +-- exactly one org) to avoid silently binding a workspace to the wrong org. +UPDATE "workspace" w +SET "organization_id" = owner_orgs."organization_id", + "workspace_mode" = 'organization'::"workspace_mode" +FROM ( + SELECT m."user_id", MIN(m."organization_id") AS "organization_id" + FROM "member" m + WHERE m."role" = 'owner' + GROUP BY m."user_id" + HAVING COUNT(*) = 1 +) AS owner_orgs +WHERE w."organization_id" IS NULL + AND w."workspace_mode" = 'grandfathered_shared' + AND w."billed_account_user_id" = owner_orgs."user_id";--> statement-breakpoint + +-- 1. Add workspace_id columns as nullable so existing rows can coexist during the data migration. +ALTER TABLE "permission_group" ADD COLUMN "workspace_id" text;--> statement-breakpoint +ALTER TABLE "permission_group" ADD CONSTRAINT "permission_group_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "permission_group_member" ADD COLUMN "workspace_id" text;--> statement-breakpoint +ALTER TABLE "permission_group_member" ADD CONSTRAINT "permission_group_member_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint + +-- 2. Materialize a plan of (source permission group, target workspace, new clone id) +-- so we can insert the clone rows AND the member rows with stable references. +CREATE TEMP TABLE "__permission_group_clone_plan" ( + "source_id" text NOT NULL, + "cloned_id" text NOT NULL, + "workspace_id" text NOT NULL +) ON COMMIT DROP;--> statement-breakpoint + +INSERT INTO "__permission_group_clone_plan" ("source_id", "cloned_id", "workspace_id") +SELECT pg."id", gen_random_uuid()::text, w."id" +FROM "permission_group" pg +JOIN "workspace" w ON w."organization_id" = pg."organization_id" +WHERE pg."organization_id" IS NOT NULL;--> statement-breakpoint + +-- 3. Create the workspace-scoped clone rows using the planned ids. +INSERT INTO "permission_group" ( + "id", + "workspace_id", + "organization_id", + "name", + "description", + "config", + "created_by", + "created_at", + "updated_at", + "auto_add_new_members" +) +SELECT + plan."cloned_id", + plan."workspace_id", + NULL, + pg."name", + pg."description", + (pg."config" - 'hideEnvironmentTab' - 'hideTemplates'), + pg."created_by", + now(), + now(), + pg."auto_add_new_members" +FROM "__permission_group_clone_plan" plan +JOIN "permission_group" pg ON pg."id" = plan."source_id";--> statement-breakpoint + +-- 4. Copy member rows to each workspace clone, populating the denormalized +-- workspace_id. Only include users who have workspace-level permissions on +-- that target workspace, OR who are the workspace owner (legacy workspaces +-- may have an owner without an explicit permissions row). +INSERT INTO "permission_group_member" ("id", "permission_group_id", "workspace_id", "user_id", "assigned_by", "assigned_at") +SELECT + gen_random_uuid()::text, + plan."cloned_id", + plan."workspace_id", + m."user_id", + m."assigned_by", + m."assigned_at" +FROM "__permission_group_clone_plan" plan +JOIN "permission_group_member" m ON m."permission_group_id" = plan."source_id" +WHERE EXISTS ( + SELECT 1 FROM "permissions" p + WHERE p."entity_type" = 'workspace' + AND p."entity_id" = plan."workspace_id" + AND p."user_id" = m."user_id" +) OR EXISTS ( + SELECT 1 FROM "workspace" w + WHERE w."id" = plan."workspace_id" + AND w."owner_id" = m."user_id" +);--> statement-breakpoint + +-- 5. Delete legacy org-scoped rows now that clones exist. +DELETE FROM "permission_group_member" +WHERE "permission_group_id" IN ( + SELECT "id" FROM "permission_group" WHERE "organization_id" IS NOT NULL +);--> statement-breakpoint + +DELETE FROM "permission_group" WHERE "organization_id" IS NOT NULL;--> statement-breakpoint + +-- 6. Enforce NOT NULL on both workspace_id columns now that every surviving row has one. +ALTER TABLE "permission_group" ALTER COLUMN "workspace_id" SET NOT NULL;--> statement-breakpoint +ALTER TABLE "permission_group_member" ALTER COLUMN "workspace_id" SET NOT NULL;--> statement-breakpoint + +-- 7. Drop legacy structures and swap indexes. +ALTER TABLE "permission_group" DROP CONSTRAINT "permission_group_organization_id_organization_id_fk";--> statement-breakpoint +DROP INDEX "permission_group_org_name_unique";--> statement-breakpoint +DROP INDEX "permission_group_org_auto_add_unique";--> statement-breakpoint +DROP INDEX "permission_group_member_user_id_unique";--> statement-breakpoint +ALTER TABLE "permission_group" DROP COLUMN "organization_id";--> statement-breakpoint +CREATE UNIQUE INDEX "permission_group_workspace_name_unique" ON "permission_group" USING btree ("workspace_id","name");--> statement-breakpoint +CREATE UNIQUE INDEX "permission_group_workspace_auto_add_unique" ON "permission_group" USING btree ("workspace_id") WHERE auto_add_new_members = true;--> statement-breakpoint +CREATE UNIQUE INDEX "permission_group_member_group_user_unique" ON "permission_group_member" USING btree ("permission_group_id","user_id");--> statement-breakpoint +CREATE UNIQUE INDEX "permission_group_member_workspace_user_unique" ON "permission_group_member" USING btree ("workspace_id","user_id");--> statement-breakpoint + +-- 8. Sweep any residual dead config keys from pre-existing workspace-scoped rows (if any). +UPDATE "permission_group" SET "config" = ("config" - 'hideEnvironmentTab' - 'hideTemplates') WHERE "config" ? 'hideEnvironmentTab' OR "config" ? 'hideTemplates'; + diff --git a/packages/db/migrations/meta/0194_snapshot.json b/packages/db/migrations/meta/0194_snapshot.json new file mode 100644 index 00000000000..64f6677e42b --- /dev/null +++ b/packages/db/migrations/meta/0194_snapshot.json @@ -0,0 +1,15210 @@ +{ + "id": "dce19071-748a-432a-86f3-6dd4021df35f", + "prevId": "8caf2eaf-a548-4c7f-83ae-546b416c5d88", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.a2a_agent": { + "name": "a2a_agent", + "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": 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 + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'1.0.0'" + }, + "capabilities": { + "name": "capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "skills": { + "name": "skills", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "authentication": { + "name": "authentication", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "signatures": { + "name": "signatures", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "is_published": { + "name": "is_published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "published_at": { + "name": "published_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": { + "a2a_agent_workflow_id_idx": { + "name": "a2a_agent_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_created_by_idx": { + "name": "a2a_agent_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_workspace_workflow_unique": { + "name": "a2a_agent_workspace_workflow_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"a2a_agent\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_archived_at_idx": { + "name": "a2a_agent_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_workspace_archived_partial_idx": { + "name": "a2a_agent_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": "\"a2a_agent\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_agent_workspace_id_workspace_id_fk": { + "name": "a2a_agent_workspace_id_workspace_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "a2a_agent_workflow_id_workflow_id_fk": { + "name": "a2a_agent_workflow_id_workflow_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "a2a_agent_created_by_user_id_fk": { + "name": "a2a_agent_created_by_user_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.a2a_push_notification_config": { + "name": "a2a_push_notification_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_schemes": { + "name": "auth_schemes", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "auth_credentials": { + "name": "auth_credentials", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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": { + "a2a_push_notification_config_task_unique": { + "name": "a2a_push_notification_config_task_unique", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_push_notification_config_task_id_a2a_task_id_fk": { + "name": "a2a_push_notification_config_task_id_a2a_task_id_fk", + "tableFrom": "a2a_push_notification_config", + "tableTo": "a2a_task", + "columnsFrom": ["task_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.a2a_task": { + "name": "a2a_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "a2a_task_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'submitted'" + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "artifacts": { + "name": "artifacts", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "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()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "a2a_task_agent_id_idx": { + "name": "a2a_task_agent_id_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_session_id_idx": { + "name": "a2a_task_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_status_idx": { + "name": "a2a_task_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_execution_id_idx": { + "name": "a2a_task_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_created_at_idx": { + "name": "a2a_task_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_task_agent_id_a2a_agent_id_fk": { + "name": "a2a_task_agent_id_a2a_agent_id_fk", + "tableFrom": "a2a_task", + "tableTo": "a2a_agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "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 + }, + "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": {} + } + }, + "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": {} + } + }, + "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_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.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": "'[]'" + }, + "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": {} + } + }, + "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 + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "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 + }, + "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": {} + } + }, + "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_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_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.credential_set": { + "name": "credential_set", + "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 + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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_set_created_by_idx": { + "name": "credential_set_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_org_name_unique": { + "name": "credential_set_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": {} + }, + "credential_set_provider_id_idx": { + "name": "credential_set_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_organization_id_organization_id_fk": { + "name": "credential_set_organization_id_organization_id_fk", + "tableFrom": "credential_set", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_created_by_user_id_fk": { + "name": "credential_set_created_by_user_id_fk", + "tableFrom": "credential_set", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set_invitation": { + "name": "credential_set_invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_set_invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "accepted_by_user_id": { + "name": "accepted_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_invitation_set_id_idx": { + "name": "credential_set_invitation_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_token_idx": { + "name": "credential_set_invitation_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_status_idx": { + "name": "credential_set_invitation_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_expires_at_idx": { + "name": "credential_set_invitation_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_invitation_credential_set_id_credential_set_id_fk": { + "name": "credential_set_invitation_credential_set_id_credential_set_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_invitation_invited_by_user_id_fk": { + "name": "credential_set_invitation_invited_by_user_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_invitation_accepted_by_user_id_user_id_fk": { + "name": "credential_set_invitation_accepted_by_user_id_user_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "user", + "columnsFrom": ["accepted_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "credential_set_invitation_token_unique": { + "name": "credential_set_invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set_member": { + "name": "credential_set_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_set_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "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_set_member_user_id_idx": { + "name": "credential_set_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_unique": { + "name": "credential_set_member_unique", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_status_idx": { + "name": "credential_set_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_member_credential_set_id_credential_set_id_fk": { + "name": "credential_set_member_credential_set_id_credential_set_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_member_user_id_user_id_fk": { + "name": "credential_set_member_user_id_user_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_member_invited_by_user_id_fk": { + "name": "credential_set_member_invited_by_user_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "user", + "columnsFrom": ["invited_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.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 + }, + "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_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_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" + } + }, + "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.form": { + "name": "form", + "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": "'[]'" + }, + "show_branding": { + "name": "show_branding", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "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": { + "form_identifier_idx": { + "name": "form_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"form\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "form_workflow_id_idx": { + "name": "form_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "form_user_id_idx": { + "name": "form_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "form_archived_at_partial_idx": { + "name": "form_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"form\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "form_workflow_id_workflow_id_fk": { + "name": "form_workflow_id_workflow_id_fk", + "tableFrom": "form", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "form_user_id_user_id_fk": { + "name": "form_user_id_user_id_fk", + "tableFrom": "form", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "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 + }, + "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_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.jwks": { + "name": "jwks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "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": "no action", + "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_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 + }, + "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.oauth_access_token": { + "name": "oauth_access_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_access_token_access_token_idx": { + "name": "oauth_access_token_access_token_idx", + "columns": [ + { + "expression": "access_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_access_token_refresh_token_idx": { + "name": "oauth_access_token_refresh_token_idx", + "columns": [ + { + "expression": "refresh_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_access_token_client_id_oauth_application_client_id_fk": { + "name": "oauth_access_token_client_id_oauth_application_client_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "oauth_application", + "columnsFrom": ["client_id"], + "columnsTo": ["client_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_access_token_user_id_user_id_fk": { + "name": "oauth_access_token_user_id_user_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_access_token_access_token_unique": { + "name": "oauth_access_token_access_token_unique", + "nullsNotDistinct": false, + "columns": ["access_token"] + }, + "oauth_access_token_refresh_token_unique": { + "name": "oauth_access_token_refresh_token_unique", + "nullsNotDistinct": false, + "columns": ["refresh_token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_application": { + "name": "oauth_application", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_urls": { + "name": "redirect_urls", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "user_id": { + "name": "user_id", + "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": { + "oauth_application_client_id_idx": { + "name": "oauth_application_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_application_user_id_user_id_fk": { + "name": "oauth_application_user_id_user_id_fk", + "tableFrom": "oauth_application", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_application_client_id_unique": { + "name": "oauth_application_client_id_unique", + "nullsNotDistinct": false, + "columns": ["client_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_consent": { + "name": "oauth_consent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "consent_given": { + "name": "consent_given", + "type": "boolean", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_consent_user_client_idx": { + "name": "oauth_consent_user_client_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_consent_client_id_oauth_application_client_id_fk": { + "name": "oauth_consent_client_id_oauth_application_client_id_fk", + "tableFrom": "oauth_consent", + "tableTo": "oauth_application", + "columnsFrom": ["client_id"], + "columnsTo": ["client_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_consent_user_id_user_id_fk": { + "name": "oauth_consent_user_id_user_id_fk", + "tableFrom": "oauth_consent", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "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 + }, + "whitelabel_settings": { + "name": "whitelabel_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 + }, + "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.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": {} + } + }, + "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 + }, + "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 + } + }, + "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": {} + } + }, + "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 + }, + "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 + }, + "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()" + }, + "auto_add_new_members": { + "name": "auto_add_new_members", + "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_workspace_name_unique": { + "name": "permission_group_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": {} + }, + "permission_group_workspace_auto_add_unique": { + "name": "permission_group_workspace_auto_add_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "auto_add_new_members = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_workspace_id_workspace_id_fk": { + "name": "permission_group_workspace_id_workspace_id_fk", + "tableFrom": "permission_group", + "tableTo": "workspace", + "columnsFrom": ["workspace_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 + }, + "workspace_id": { + "name": "workspace_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_workspace_user_unique": { + "name": "permission_group_member_workspace_user_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "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_workspace_id_workspace_id_fk": { + "name": "permission_group_member_workspace_id_workspace_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "workspace", + "columnsFrom": ["workspace_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.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.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 + }, + "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 + }, + "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.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.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 + }, + "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 + }, + "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.template_creators": { + "name": "template_creators", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "reference_type": { + "name": "reference_type", + "type": "template_creator_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "profile_image_url": { + "name": "profile_image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "template_creators_reference_idx": { + "name": "template_creators_reference_idx", + "columns": [ + { + "expression": "reference_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_creators_reference_id_idx": { + "name": "template_creators_reference_id_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_creators_created_by_idx": { + "name": "template_creators_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "template_creators_created_by_user_id_fk": { + "name": "template_creators_created_by_user_id_fk", + "tableFrom": "template_creators", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.template_stars": { + "name": "template_stars", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "starred_at": { + "name": "starred_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "template_stars_user_id_idx": { + "name": "template_stars_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_id_idx": { + "name": "template_stars_template_id_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_user_template_idx": { + "name": "template_stars_user_template_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_user_idx": { + "name": "template_stars_template_user_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_starred_at_idx": { + "name": "template_stars_starred_at_idx", + "columns": [ + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_starred_at_idx": { + "name": "template_stars_template_starred_at_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_user_template_unique": { + "name": "template_stars_user_template_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "template_stars_user_id_user_id_fk": { + "name": "template_stars_user_id_user_id_fk", + "tableFrom": "template_stars", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "template_stars_template_id_templates_id_fk": { + "name": "template_stars_template_id_templates_id_fk", + "tableFrom": "template_stars", + "tableTo": "templates", + "columnsFrom": ["template_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.templates": { + "name": "templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "views": { + "name": "views", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "stars": { + "name": "stars", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "template_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "required_credentials": { + "name": "required_credentials", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "state": { + "name": "state", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "og_image_url": { + "name": "og_image_url", + "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": { + "templates_status_idx": { + "name": "templates_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_creator_id_idx": { + "name": "templates_creator_id_idx", + "columns": [ + { + "expression": "creator_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_views_idx": { + "name": "templates_views_idx", + "columns": [ + { + "expression": "views", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_stars_idx": { + "name": "templates_stars_idx", + "columns": [ + { + "expression": "stars", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_status_views_idx": { + "name": "templates_status_views_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "views", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_status_stars_idx": { + "name": "templates_status_stars_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stars", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_created_at_idx": { + "name": "templates_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_updated_at_idx": { + "name": "templates_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "templates_workflow_id_workflow_id_fk": { + "name": "templates_workflow_id_workflow_id_fk", + "tableFrom": "templates", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "templates_creator_id_template_creators_id_fk": { + "name": "templates_creator_id_template_creators_id_fk", + "tableFrom": "templates", + "tableTo": "template_creators", + "columnsFrom": ["creator_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 + }, + "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_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": {} + } + }, + "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": {}, + "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_a2a_executions": { + "name": "total_a2a_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 + } + }, + "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 + }, + "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 + }, + "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_table_id_idx": { + "name": "user_table_rows_table_id_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_data_gin_idx": { + "name": "user_table_rows_data_gin_idx", + "columns": [ + { + "expression": "data", + "isExpression": false, + "asc": 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": {} + } + }, + "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 + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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 + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "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": {} + }, + "idx_webhook_on_workflow_id_block_id": { + "name": "idx_webhook_on_workflow_id_block_id", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "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_credential_set_id_idx": { + "name": "webhook_credential_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "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": {} + } + }, + "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" + }, + "webhook_credential_set_id_credential_set_id_fk": { + "name": "webhook_credential_set_id_credential_set_id_fk", + "tableFrom": "webhook", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "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 + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#3972F6'" + }, + "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 + }, + "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_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 + }, + "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_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": {} + } + }, + "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 + }, + "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": "'{}'" + }, + "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 + }, + "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 + }, + "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 + }, + "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": {} + } + }, + "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_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 + }, + "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 + }, + "log_retention_hours": { + "name": "log_retention_hours", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "soft_delete_retention_hours": { + "name": "soft_delete_retention_hours", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "task_cleanup_hours": { + "name": "task_cleanup_hours", + "type": "integer", + "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": { + "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": {} + } + }, + "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" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "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 + }, + "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_provider_unique": { + "name": "workspace_byok_provider_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_byok_workspace_idx": { + "name": "workspace_byok_workspace_idx", + "columns": [ + { + "expression": "workspace_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_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 + }, + "context": { + "name": "context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "original_name": { + "name": "original_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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()" + } + }, + "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_name_active_unique": { + "name": "workspace_files_workspace_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": 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_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_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_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_notification_delivery": { + "name": "workspace_notification_delivery", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "subscription_id": { + "name": "subscription_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "notification_delivery_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "response_status": { + "name": "response_status", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "response_body": { + "name": "response_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "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_notification_delivery_subscription_id_idx": { + "name": "workspace_notification_delivery_subscription_id_idx", + "columns": [ + { + "expression": "subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_delivery_execution_id_idx": { + "name": "workspace_notification_delivery_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_delivery_status_idx": { + "name": "workspace_notification_delivery_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_delivery_next_attempt_idx": { + "name": "workspace_notification_delivery_next_attempt_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_notification_delivery_subscription_id_workspace_notification_subscription_id_fk": { + "name": "workspace_notification_delivery_subscription_id_workspace_notification_subscription_id_fk", + "tableFrom": "workspace_notification_delivery", + "tableTo": "workspace_notification_subscription", + "columnsFrom": ["subscription_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_notification_delivery_workflow_id_workflow_id_fk": { + "name": "workspace_notification_delivery_workflow_id_workflow_id_fk", + "tableFrom": "workspace_notification_delivery", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_notification_subscription": { + "name": "workspace_notification_subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "notification_type": { + "name": "notification_type", + "type": "notification_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "workflow_ids": { + "name": "workflow_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "all_workflows": { + "name": "all_workflows", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "level_filter": { + "name": "level_filter", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY['info', 'error']::text[]" + }, + "trigger_filter": { + "name": "trigger_filter", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY['api', 'webhook', 'schedule', 'manual', 'chat']::text[]" + }, + "include_final_output": { + "name": "include_final_output", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_trace_spans": { + "name": "include_trace_spans", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_rate_limits": { + "name": "include_rate_limits", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_usage_data": { + "name": "include_usage_data", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "webhook_config": { + "name": "webhook_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "email_recipients": { + "name": "email_recipients", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "slack_config": { + "name": "slack_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "alert_config": { + "name": "alert_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_alert_at": { + "name": "last_alert_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "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": { + "workspace_notification_workspace_id_idx": { + "name": "workspace_notification_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_active_idx": { + "name": "workspace_notification_active_idx", + "columns": [ + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_type_idx": { + "name": "workspace_notification_type_idx", + "columns": [ + { + "expression": "notification_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_notification_subscription_workspace_id_workspace_id_fk": { + "name": "workspace_notification_subscription_workspace_id_workspace_id_fk", + "tableFrom": "workspace_notification_subscription", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_notification_subscription_created_by_user_id_fk": { + "name": "workspace_notification_subscription_created_by_user_id_fk", + "tableFrom": "workspace_notification_subscription", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.a2a_task_status": { + "name": "a2a_task_status", + "schema": "public", + "values": [ + "submitted", + "working", + "input-required", + "completed", + "failed", + "canceled", + "rejected", + "auth-required", + "unknown" + ] + }, + "public.academy_cert_status": { + "name": "academy_cert_status", + "schema": "public", + "values": ["active", "revoked", "expired"] + }, + "public.billing_blocked_reason": { + "name": "billing_blocked_reason", + "schema": "public", + "values": ["payment_failed", "dispute"] + }, + "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_set_invitation_status": { + "name": "credential_set_invitation_status", + "schema": "public", + "values": ["pending", "accepted", "expired", "cancelled"] + }, + "public.credential_set_member_status": { + "name": "credential_set_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.invitation_kind": { + "name": "invitation_kind", + "schema": "public", + "values": ["organization", "workspace"] + }, + "public.invitation_status": { + "name": "invitation_status", + "schema": "public", + "values": ["pending", "accepted", "rejected", "cancelled", "expired"] + }, + "public.notification_delivery_status": { + "name": "notification_delivery_status", + "schema": "public", + "values": ["pending", "in_progress", "success", "failed"] + }, + "public.notification_type": { + "name": "notification_type", + "schema": "public", + "values": ["webhook", "email", "slack"] + }, + "public.permission_type": { + "name": "permission_type", + "schema": "public", + "values": ["admin", "write", "read"] + }, + "public.template_creator_type": { + "name": "template_creator_type", + "schema": "public", + "values": ["user", "organization"] + }, + "public.template_status": { + "name": "template_status", + "schema": "public", + "values": ["pending", "approved", "rejected"] + }, + "public.usage_log_category": { + "name": "usage_log_category", + "schema": "public", + "values": ["model", "fixed"] + }, + "public.usage_log_source": { + "name": "usage_log_source", + "schema": "public", + "values": [ + "workflow", + "wand", + "copilot", + "workspace-chat", + "mcp_copilot", + "mothership_block", + "knowledge-base", + "voice-input" + ] + }, + "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 0941b512d3c..64fd8b510c4 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -1352,6 +1352,13 @@ "when": 1776725749082, "tag": "0193_lying_rocket_racer", "breakpoints": true + }, + { + "idx": 194, + "version": "7", + "when": 1776800382630, + "tag": "0194_careless_pete_wisdom", + "breakpoints": true } ] } diff --git a/packages/db/schema.ts b/packages/db/schema.ts index 22a9bffe61a..037649b75f7 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -2646,9 +2646,9 @@ export const permissionGroup = pgTable( 'permission_group', { id: text('id').primaryKey(), - organizationId: text('organization_id') + workspaceId: text('workspace_id') .notNull() - .references(() => organization.id, { onDelete: 'cascade' }), + .references(() => workspace.id, { onDelete: 'cascade' }), name: text('name').notNull(), description: text('description'), config: jsonb('config').notNull().default('{}'), @@ -2661,12 +2661,12 @@ export const permissionGroup = pgTable( }, (table) => ({ createdByIdx: index('permission_group_created_by_idx').on(table.createdBy), - orgNameUnique: uniqueIndex('permission_group_org_name_unique').on( - table.organizationId, + workspaceNameUnique: uniqueIndex('permission_group_workspace_name_unique').on( + table.workspaceId, table.name ), - autoAddNewMembersUnique: uniqueIndex('permission_group_org_auto_add_unique') - .on(table.organizationId) + autoAddNewMembersUnique: uniqueIndex('permission_group_workspace_auto_add_unique') + .on(table.workspaceId) .where(sql`auto_add_new_members = true`), }) ) @@ -2678,6 +2678,9 @@ export const permissionGroupMember = pgTable( permissionGroupId: text('permission_group_id') .notNull() .references(() => permissionGroup.id, { onDelete: 'cascade' }), + workspaceId: text('workspace_id') + .notNull() + .references(() => workspace.id, { onDelete: 'cascade' }), userId: text('user_id') .notNull() .references(() => user.id, { onDelete: 'cascade' }), @@ -2686,7 +2689,14 @@ export const permissionGroupMember = pgTable( }, (table) => ({ permissionGroupIdIdx: index('permission_group_member_group_id_idx').on(table.permissionGroupId), - userIdUnique: uniqueIndex('permission_group_member_user_id_unique').on(table.userId), + groupUserUnique: uniqueIndex('permission_group_member_group_user_unique').on( + table.permissionGroupId, + table.userId + ), + workspaceUserUnique: uniqueIndex('permission_group_member_workspace_user_unique').on( + table.workspaceId, + table.userId + ), }) ) diff --git a/packages/utils/src/errors.ts b/packages/utils/src/errors.ts index 73d05d90e31..5bfc309c1c5 100644 --- a/packages/utils/src/errors.ts +++ b/packages/utils/src/errors.ts @@ -13,6 +13,19 @@ export function toError(value: unknown): Error { * Normalizes common Drizzle / `postgres` driver shapes and walks `cause` chains. */ export function getPostgresErrorCode(error: unknown): string | undefined { + return readPgErrorField(error, 'code') +} + +/** + * Returns the name of the PostgreSQL constraint that triggered the error (e.g. the unique index + * name on a `23505`), when present on a thrown value. Mirrors the field populated by the + * `postgres` / `pg` drivers, walking `cause` chains the same way as `getPostgresErrorCode`. + */ +export function getPostgresConstraintName(error: unknown): string | undefined { + return readPgErrorField(error, 'constraint_name') ?? readPgErrorField(error, 'constraint') +} + +function readPgErrorField(error: unknown, field: string): string | undefined { const seen = new Set() let current: unknown = error @@ -23,9 +36,9 @@ export function getPostgresErrorCode(error: unknown): string | undefined { seen.add(current) if (typeof current === 'object') { - const code = (current as { code?: unknown }).code - if (typeof code === 'string') { - return code + const value = (current as Record)[field] + if (typeof value === 'string') { + return value } }