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

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

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

- {tool.schema.function.description} -

- )} -
- {canEdit && ( -
- setEditingTool(tool.id) }, - { - label: 'Delete', - destructive: true, - disabled: deletingTools.has(tool.id), - onSelect: () => handleDeleteClick(tool.id), - }, - ]} - /> -
- )} -
- ))} - {showNoResults && ( - - No tools found matching "{searchTerm}" - - )} -
- )} -
+ /** + * Hold the first paint while a deep-linked id could still resolve — the tools + * query and the workspace permissions context both gate the detail, so a valid + * link never flashes the list before jumping to it. A dead id still falls back + * to the list. + */ + if (selectedToolId !== null && (isLoading || workspacePermissions.isLoading)) return null - {canEdit && ( - { - if (!open) { - setShowAddForm(false) - setEditingTool(null) - } - }} - onSave={handleToolSaved} - onDelete={() => {}} - blockId='' - initialValues={ - editingTool - ? (() => { - const tool = tools.find((t) => t.id === editingTool) - return tool?.schema - ? { id: tool.id, schema: tool.schema, code: tool.code } - : undefined - })() - : undefined - } - /> - )} + if ((isCreating && canEdit) || selectedTool) { + return ( + { + setIsCreating(false) + void setSelectedToolId(toolId) + }} + /> + ) + } - {canEdit && ( - { - if (!open) setShowDeleteDialog(false) - }} - srTitle='Delete Custom Tool' - title='Delete Custom Tool' - text={[ - 'Are you sure you want to delete ', - { text: toolToDelete?.name ?? 'this tool', bold: true }, - '? This action cannot be undone.', - ]} - confirm={{ label: 'Delete', onClick: handleDeleteTool }} - /> + return ( + + {error ? ( +
+

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

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

Loading…

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

Skill not found.

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

{errors.name}

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

{errors.description}

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

{errors.content}

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

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

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

{errors.description}

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

{errors.content}

+ )} +
+
+ + + + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/skills/search-params.ts b/apps/sim/app/workspace/[workspaceId]/skills/search-params.ts index 554005fc76d..e78b8167fc5 100644 --- a/apps/sim/app/workspace/[workspaceId]/skills/search-params.ts +++ b/apps/sim/app/workspace/[workspaceId]/skills/search-params.ts @@ -3,20 +3,18 @@ import { parseAsString } from 'nuqs/server' /** * Co-located, typed URL query-param definition for the Skills gallery. * - * `skillId` deep-links the skill edit modal to a specific skill. The modal - * opens when a valid id (one that resolves to a loaded skill) is present; - * closing it clears the param. Opening a skill is a destination, so it lands in - * the browser history (`history: 'push'`). The "create new skill" flow has no id - * and stays in local component state. + * `skillId` is a LEGACY deep-link param from when skills edited in a modal on + * this page. Skills now open at `/workspace/[workspaceId]/skills/[skillId]`; + * the gallery reads this param once and redirects there (read-then-strip). */ export const skillIdParam = { key: 'skillId', parser: parseAsString, } as const -/** Opening a skill is a destination → push to history; clear on close. */ +/** Read-once redirect signal — replace history, never linger. */ export const skillIdUrlKeys = { - history: 'push', + history: 'replace', clearOnDefault: true, } as const diff --git a/apps/sim/app/workspace/[workspaceId]/skills/skills.tsx b/apps/sim/app/workspace/[workspaceId]/skills/skills.tsx index d045bf88b1b..7004a6c3ebf 100644 --- a/apps/sim/app/workspace/[workspaceId]/skills/skills.tsx +++ b/apps/sim/app/workspace/[workspaceId]/skills/skills.tsx @@ -1,27 +1,23 @@ 'use client' -import { useState } from 'react' -import { Chip, ChipConfirmModal, ChipInput, Search } from '@sim/emcn' -import { createLogger } from '@sim/logger' +import { useEffect, useRef } from 'react' +import { Chip, ChipInput, Search } from '@sim/emcn' import { getErrorMessage } from '@sim/utils/errors' import { ArrowRight, Plus } from 'lucide-react' -import { useParams } from 'next/navigation' +import { useParams, useRouter } from 'next/navigation' import { useQueryState } from 'nuqs' import { SkillTile } from '@/app/workspace/[workspaceId]/components' import { IntegrationTabsHeader } from '@/app/workspace/[workspaceId]/integrations/components/integration-tabs-header' import { ShowcaseWithExplore } from '@/app/workspace/[workspaceId]/integrations/components/showcase-with-explore' -import { SkillModal } from '@/app/workspace/[workspaceId]/skills/components/skill-modal' import { skillIdParam, skillIdUrlKeys, skillSearchParam, skillSearchUrlKeys, } from '@/app/workspace/[workspaceId]/skills/search-params' -import { useDeleteSkill, useSkills } from '@/hooks/queries/skills' +import { useSkills } from '@/hooks/queries/skills' import { useDebouncedSearchSetter } from '@/hooks/use-debounced-search-setter' -const logger = createLogger('SkillsSettings') - const SKILLS_LABEL = 'Skills' interface SkillItemProps { @@ -68,22 +64,31 @@ function SkillSection({ label, children }: SkillSectionProps) { export function Skills() { const params = useParams() + const router = useRouter() const workspaceId = (params?.workspaceId as string) || '' + const skillsHref = `/workspace/${workspaceId}/skills` const { data: skills = [], isLoading, error } = useSkills(workspaceId) - const deleteSkillMutation = useDeleteSkill() const [searchTerm, setSearchTermParam] = useQueryState(skillSearchParam.key, { ...skillSearchParam.parser, ...skillSearchUrlKeys, }) - const [editingSkillId, setEditingSkillId] = useQueryState(skillIdParam.key, { + const [legacySkillId, setLegacySkillId] = useQueryState(skillIdParam.key, { ...skillIdParam.parser, ...skillIdUrlKeys, }) - const [showAddForm, setShowAddForm] = useState(false) - const [skillToDelete, setSkillToDelete] = useState<{ id: string; name: string } | null>(null) - const [showDeleteDialog, setShowDeleteDialog] = useState(false) + /** + * Legacy deep links opened the edit modal via `?skillId=`; skills now have a + * dedicated detail page. Redirect once, stripping the param. + */ + const redirectedLegacyId = useRef(false) + useEffect(() => { + if (!legacySkillId || redirectedLegacyId.current) return + redirectedLegacyId.current = true + setLegacySkillId(null, { history: 'replace' }) + router.replace(`${skillsHref}/${legacySkillId}`) + }, [legacySkillId, setLegacySkillId, router, skillsHref]) /** * The input is controlled directly by the instant nuqs value; only the URL @@ -92,9 +97,6 @@ export function Skills() { */ const setSearchTerm = useDebouncedSearchSetter(setSearchTermParam) - /** Derive the skill being edited from the loaded list — never store the object in the URL. */ - const editingSkill = editingSkillId ? (skills.find((s) => s.id === editingSkillId) ?? null) : null - const filteredSkills = skills.filter((s) => { if (!searchTerm.trim()) return true const searchLower = searchTerm.trim().toLowerCase() @@ -104,46 +106,15 @@ export function Skills() { ) }) - const handleDeleteClick = (skillId: string) => { - const s = skills.find((sk) => sk.id === skillId) - if (!s) return - - setSkillToDelete({ id: skillId, name: s.name }) - setShowDeleteDialog(true) - } - - const handleDeleteSkill = async () => { - if (!skillToDelete) return - - setShowDeleteDialog(false) - - try { - await deleteSkillMutation.mutateAsync({ - workspaceId, - skillId: skillToDelete.id, - }) - logger.info(`Deleted skill: ${skillToDelete.id}`) - } catch (error) { - logger.error('Error deleting skill:', error) - } finally { - setSkillToDelete(null) - } - } - - const handleSkillSaved = () => { - setShowAddForm(false) - setEditingSkillId(null) - } - const showNoResults = searchTerm.trim() && filteredSkills.length === 0 - const handleOpenAddForm = () => { - setEditingSkillId(null) - setShowAddForm(true) - } - const addButton = ( - + router.push(`${skillsHref}/new`)} + disabled={isLoading} + leftIcon={Plus} + > Add to Sim ) @@ -177,7 +148,7 @@ export function Skills() { key={s.id} name={s.name} description={s.description} - onClick={() => setEditingSkillId(s.id)} + onClick={() => router.push(`${skillsHref}/${s.id}`)} /> ))} @@ -189,38 +160,6 @@ export function Skills() {
- - { - if (!open) { - setShowAddForm(false) - setEditingSkillId(null) - } - }} - onSave={handleSkillSaved} - onDelete={(skillId) => { - setEditingSkillId(null) - handleDeleteClick(skillId) - }} - initialValues={editingSkill ?? undefined} - /> - -
) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/skill-input/skill-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/skill-input/skill-input.tsx index 80be87af9ea..ac2282c5b62 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/skill-input/skill-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/skill-input/skill-input.tsx @@ -10,8 +10,7 @@ import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/ import { getWorkflowSearchLabelHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' import { useActiveSearchTarget } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider' -import type { SkillDefinition } from '@/hooks/queries/skills' -import { useSkills } from '@/hooks/queries/skills' +import { type SkillDefinition, useSkills } from '@/hooks/queries/skills' import { usePermissionConfig } from '@/hooks/use-permission-config' interface StoredSkill { @@ -42,7 +41,16 @@ export function SkillInput({ const { data: workspaceSkills = [] } = useSkills(workspaceId) const [value, setValue] = useSubBlockValue(blockId, subBlockId) const [showCreateModal, setShowCreateModal] = useState(false) - const [editingSkill, setEditingSkill] = useState(null) + const [editingSkillId, setEditingSkillId] = useState(null) + const [editingSkillSnapshot, setEditingSkillSnapshot] = useState(null) + + // Prefer the live query cache so the modal reflects concurrent edits, but + // fall back to the click-time snapshot when a background refetch drops the + // skill — otherwise the modal would close mid-edit and silently discard the + // draft; saving surfaces the real server error instead. + const editingSkill = editingSkillId + ? (workspaceSkills.find((s) => s.id === editingSkillId) ?? editingSkillSnapshot) + : null const selectedSkills: StoredSkill[] = useMemo(() => { if (isPreview && previewValue) { @@ -105,7 +113,8 @@ export function SkillInput({ const handleSkillSaved = useCallback(() => { setShowCreateModal(false) - setEditingSkill(null) + setEditingSkillId(null) + setEditingSkillSnapshot(null) }, []) const resolveSkillName = useCallback( @@ -150,7 +159,8 @@ export function SkillInput({ className='flex cursor-pointer items-center justify-between gap-2 rounded-t-[4px] bg-[var(--surface-4)] px-2 py-[6.5px]' onClick={() => { if (fullSkill && !disabled && !isPreview) { - setEditingSkill(fullSkill) + setEditingSkillId(fullSkill.id) + setEditingSkillSnapshot(fullSkill) } }} > @@ -188,7 +198,8 @@ export function SkillInput({ onOpenChange={(isOpen) => { if (!isOpen) { setShowCreateModal(false) - setEditingSkill(null) + setEditingSkillId(null) + setEditingSkillSnapshot(null) } }} onSave={handleSkillSaved} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/code-editor/code-editor.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/code-editor/code-editor.tsx index a6df56db169..61012a92c35 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/code-editor/code-editor.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/code-editor/code-editor.tsx @@ -4,14 +4,14 @@ import { CODE_LINE_HEIGHT_PX, Code, calculateGutterWidth, + chipFieldSurfaceClass, cn, getCodeEditorProps, highlight, languages, } from '@sim/emcn' -import { Wand2 } from 'lucide-react' import Editor from 'react-simple-code-editor' -import { Button } from '@/components/ui/button' +import type { SchemaParameter } from '@/app/workspace/[workspaceId]/components/custom-tool-editor/custom-tool-schema' import { createEnvVarPattern, createWorkflowVariablePattern, @@ -22,16 +22,15 @@ interface CodeEditorProps { onChange: (value: string) => void language: 'javascript' | 'json' placeholder?: string + /** Layout/sizing only — the chip-field chrome is owned by this component. */ className?: string - gutterClassName?: string + /** Swaps the field border to the error token. */ + error?: boolean minHeight?: string highlightVariables?: boolean onKeyDown?: (e: React.KeyboardEvent) => void disabled?: boolean - schemaParameters?: Array<{ name: string; type: string; description: string; required: boolean }> - showWandButton?: boolean - onWandClick?: () => void - wandButtonDisabled?: boolean + schemaParameters?: SchemaParameter[] } const EMPTY_SCHEMA_PARAMETERS: NonNullable = [] @@ -42,15 +41,12 @@ export function CodeEditor({ language, placeholder = '', className = '', - gutterClassName = '', + error = false, minHeight, highlightVariables = true, onKeyDown, disabled = false, schemaParameters = EMPTY_SCHEMA_PARAMETERS, - showWandButton = false, - onWandClick, - wandButtonDisabled = false, }: CodeEditorProps) { const [visualLineHeights, setVisualLineHeights] = useState([]) @@ -182,21 +178,11 @@ export function CodeEditor({ } return ( - - {showWandButton && onWandClick && ( - - )} - - + + {renderLineNumbers()} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/custom-tool-modal/custom-tool-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/custom-tool-modal/custom-tool-modal.tsx index ab399baa0de..10dc434c4ae 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/custom-tool-modal/custom-tool-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/custom-tool-modal/custom-tool-modal.tsx @@ -1,37 +1,30 @@ -import { useEffect, useMemo, useRef, useState } from 'react' +'use client' + +import { useEffect, useMemo, useState } from 'react' import { - Badge, - Button, ChipConfirmModal, ChipModal, ChipModalBody, ChipModalFooter, ChipModalHeader, ChipModalTabs, - cn, - Input, - Label, - Popover, - PopoverAnchor, - PopoverContent, - PopoverItem, - PopoverScrollArea, - PopoverSection, + toast, } from '@sim/emcn' import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' -import { AlertCircle, ArrowUp } from 'lucide-react' import { useParams } from 'next/navigation' import { - checkEnvVarTrigger, - EnvVarDropdown, -} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/env-var-dropdown' -import { - checkTagTrigger, - TagDropdown, -} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown' -import { CodeEditor } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/code-editor/code-editor' -import { useWand } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-wand' + CUSTOM_TOOL_DELETE_CONFIRM_TEXT, + CustomToolCodeField, + CustomToolSchemaField, + extractSchemaParameters, + FieldErrorText, + FUNCTION_NAME_LOCKED, + GeneratePromptControl, + useCodeGeneration, + useSchemaGeneration, + validateCustomToolSchema, +} from '@/app/workspace/[workspaceId]/components/custom-tool-editor' import { useCreateCustomTool, useCustomTools, @@ -68,6 +61,11 @@ export interface CustomTool { type ToolSection = 'schema' | 'code' +const TOOL_TABS = [ + { value: 'schema', label: 'Schema' }, + { value: 'code', label: 'Code' }, +] as const + export function CustomToolModal({ open, onOpenChange, @@ -89,216 +87,34 @@ export function CustomToolModal({ const [initialFunctionCode, setInitialFunctionCode] = useState('') const [showDeleteConfirm, setShowDeleteConfirm] = useState(false) const [showDiscardAlert, setShowDiscardAlert] = useState(false) - const [isSchemaPromptActive, setIsSchemaPromptActive] = useState(false) - const [schemaPromptInput, setSchemaPromptInput] = useState('') - const schemaPromptInputRef = useRef(null) - - const [isCodePromptActive, setIsCodePromptActive] = useState(false) - const [codePromptInput, setCodePromptInput] = useState('') - const codePromptInputRef = useRef(null) - - const schemaGeneration = useWand({ - wandConfig: { - enabled: true, - maintainHistory: true, - prompt: `You are an expert programmer specializing in creating OpenAI function calling format JSON schemas for custom tools. -Generate ONLY the JSON schema based on the user's request. -The output MUST be a single, valid JSON object, starting with { and ending with }. -The JSON schema MUST follow this specific format: -1. Top-level property "type" must be set to "function" -2. A "function" object containing: - - "name": A concise, camelCase name for the function - - "description": A clear description of what the function does - - "parameters": A JSON Schema object describing the function's parameters with: - - "type": "object" - - "properties": An object containing parameter definitions - - "required": An array of required parameter names -Current schema: {context} - -Do not include any explanations, markdown formatting, or other text outside the JSON object. - -Valid Schema Examples: - -Example 1: -{ - "type": "function", - "function": { - "name": "getWeather", - "description": "Fetches the current weather for a specific location.", - "parameters": { - "type": "object", - "properties": { - "location": { - "type": "string", - "description": "The city and state, e.g., San Francisco, CA" - }, - "unit": { - "type": "string", - "description": "Temperature unit", - "enum": ["celsius", "fahrenheit"] - } - }, - "required": ["location"], - "additionalProperties": false - } - } -} + const schemaParameters = useMemo(() => extractSchemaParameters(jsonSchema), [jsonSchema]) -Example 2: -{ - "type": "function", - "function": { - "name": "addItemToOrder", - "description": "Add one quantity of a food item to the order.", - "parameters": { - "type": "object", - "properties": { - "itemName": { - "type": "string", - "description": "The name of the food item to add to order" - }, - "quantity": { - "type": "integer", - "description": "The quantity of the item to add", - "default": 1 - } - }, - "required": ["itemName"], - "additionalProperties": false - } - } -}`, - placeholder: 'Describe the function parameters and structure...', - generationType: 'custom-tool-schema', - }, - currentValue: jsonSchema, - onStreamStart: () => { - setJsonSchema('') - }, - onGeneratedContent: (content) => { - setJsonSchema(content) + const schemaGeneration = useSchemaGeneration({ + jsonSchema, + setJsonSchema: (updater) => { + setJsonSchema(updater) setSchemaError(null) }, - onStreamChunk: (chunk) => { - setJsonSchema((prev) => { - const newSchema = prev + chunk - if (schemaError) setSchemaError(null) - return newSchema - }) + replaceJsonSchema: (value) => { + setJsonSchema(value) + setSchemaError(null) }, }) - const schemaParameters = useMemo(() => { - try { - if (!jsonSchema) return [] - const parsed = JSON.parse(jsonSchema) - const properties = parsed?.function?.parameters?.properties - if (!properties) return [] - - return Object.keys(properties).map((key) => ({ - name: key, - type: properties[key].type || 'any', - description: properties[key].description || '', - required: parsed?.function?.parameters?.required?.includes(key) || false, - })) - } catch { - return [] - } - }, [jsonSchema]) - - const codeGenerationSchemaContext = useMemo(() => { - if (schemaParameters.length === 0) { - return 'Schema parameters: (none defined yet — the user has not added any parameters to the schema)' - } - const lines = schemaParameters.map((p) => { - const requiredLabel = p.required ? 'required' : 'optional' - const description = p.description ? `: ${p.description}` : '' - return `- ${p.name} (${p.type}, ${requiredLabel})${description}` - }) - return `Schema parameters (reference these directly by name in the generated code):\n${lines.join('\n')}` - }, [schemaParameters]) - - const codeGeneration = useWand({ - wandConfig: { - enabled: true, - maintainHistory: true, - prompt: `You are an expert JavaScript programmer. -Generate ONLY the raw body of a JavaScript function based on the user's request. -The code should be executable within an 'async function(params, environmentVariables) {...}' context. -- 'params' (object): Contains input parameters derived from the JSON schema. Reference these directly by name (e.g., 'userId', 'cityName'). Do NOT use 'params.paramName'. -- 'environmentVariables' (object): Contains environment variables. Reference these using the double curly brace syntax: '{{ENV_VAR_NAME}}'. Do NOT use 'environmentVariables.VAR_NAME' or env. - -${codeGenerationSchemaContext} - -Current code: {context} - -IMPORTANT FORMATTING RULES: -1. Reference Environment Variables: Use the exact syntax {{VARIABLE_NAME}}. Do NOT wrap it in quotes (e.g., use 'const apiKey = {{SERVICE_API_KEY}};' not 'const apiKey = "{{SERVICE_API_KEY}}";'). Our system replaces these placeholders before execution. -2. Reference Input Parameters/Workflow Variables: Reference them directly by name (e.g., 'const city = cityName;' or use directly in template strings like \`\${cityName}\`). Do NOT wrap in quotes or angle brackets. -3. Function Body ONLY: Do NOT include the function signature (e.g., 'async function myFunction() {' or the surrounding '}'). -4. Imports: Do NOT include import/require statements unless they are standard Node.js built-in modules (e.g., 'crypto', 'fs'). External libraries are not supported in this context. -5. Output: Ensure the code returns a value if the function is expected to produce output. Use 'return'. -6. Clarity: Write clean, readable code. -7. No Explanations: Do NOT include markdown formatting, comments explaining the rules, or any text other than the raw JavaScript code for the function body. - -Example Scenario: -User Prompt: "Fetch weather data from OpenWeather API. Use the city name passed in as 'cityName' and an API Key stored as the 'OPENWEATHER_API_KEY' environment variable." - -Generated Code: -const apiKey = {{OPENWEATHER_API_KEY}}; -const url = \`https://api.openweathermap.org/data/2.5/weather?q=\${cityName}&appid=\${apiKey}\`; - -try { - const response = await fetch(url, { - method: 'GET', - headers: { - 'Content-Type': 'application/json' - } - }); - - if (!response.ok) { - throw new Error(\`API request failed with status \${response.status}: \${await response.text()}\`); - } - - const weatherData = await response.json(); - return weatherData; -} catch (error) { - console.error(\`Error fetching weather data: \${error.message}\`); - throw error; -}`, - placeholder: 'Describe the JavaScript function to generate...', - generationType: 'javascript-function-body', - }, - currentValue: functionCode, - onStreamStart: () => { - setFunctionCode('') - }, - onGeneratedContent: (content) => { - handleFunctionCodeChange(content) + const codeGeneration = useCodeGeneration({ + functionCode, + schemaParameters, + setFunctionCode: (updater) => { + setFunctionCode(updater) setCodeError(null) }, - onStreamChunk: (chunk) => { - setFunctionCode((prev) => { - const newCode = prev + chunk - handleFunctionCodeChange(newCode) - if (codeError) setCodeError(null) - return newCode - }) + replaceFunctionCode: (value) => { + setFunctionCode(value) + setCodeError(null) }, }) - const [showEnvVars, setShowEnvVars] = useState(false) - const [showTags, setShowTags] = useState(false) - const [showSchemaParams, setShowSchemaParams] = useState(false) - const [searchTerm, setSearchTerm] = useState('') - const [cursorPosition, setCursorPosition] = useState(0) - const codeEditorRef = useRef(null) - const [activeSourceBlockId, setActiveSourceBlockId] = useState(null) - const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0 }) - const [schemaParamSelectedIndex, setSchemaParamSelectedIndex] = useState(0) - const schemaParamItemRefs = useRef>(new Map()) - const createToolMutation = useCreateCustomTool() const updateToolMutation = useUpdateCustomTool() const deleteToolMutation = useDeleteCustomTool() @@ -329,18 +145,6 @@ try { } }, [open]) - useEffect(() => { - if (!showSchemaParams || schemaParamSelectedIndex < 0) return - - const element = schemaParamItemRefs.current.get(schemaParamSelectedIndex) - if (element) { - element.scrollIntoView({ - behavior: 'smooth', - block: 'nearest', - }) - } - }, [schemaParamSelectedIndex, showSchemaParams]) - const resetForm = () => { setJsonSchema('') setFunctionCode('') @@ -351,15 +155,7 @@ try { setActiveSection('schema') setIsEditing(false) setToolId(undefined) - setIsSchemaPromptActive(false) - setIsCodePromptActive(false) - setSchemaPromptInput('') - setCodePromptInput('') setShowDiscardAlert(false) - schemaGeneration.closePrompt() - schemaGeneration.hidePromptInline() - codeGeneration.closePrompt() - codeGeneration.hidePromptInline() } const handleClose = () => { @@ -369,53 +165,18 @@ try { onOpenChange(false) } - const validateSchema = (schema: string): { isValid: boolean; error: string | null } => { - if (!schema) return { isValid: false, error: null } - - try { - const parsed = JSON.parse(schema) - - if (!parsed.type || parsed.type !== 'function') { - return { isValid: false, error: 'Missing "type": "function"' } - } - if (!parsed.function || !parsed.function.name) { - return { isValid: false, error: 'Missing function.name field' } - } - if (!parsed.function.parameters) { - return { isValid: false, error: 'Missing function.parameters object' } - } - if (!parsed.function.parameters.type) { - return { isValid: false, error: 'Missing parameters.type field' } - } - if (parsed.function.parameters.properties === undefined) { - return { isValid: false, error: 'Missing parameters.properties field' } - } - if ( - typeof parsed.function.parameters.properties !== 'object' || - parsed.function.parameters.properties === null - ) { - return { isValid: false, error: 'parameters.properties must be an object' } - } - - return { isValid: true, error: null } - } catch { - return { isValid: false, error: 'Invalid JSON format' } - } - } - - const isSchemaValid = useMemo(() => validateSchema(jsonSchema).isValid, [jsonSchema]) + /** The Generate control and error live in the shared tab row, so both follow the visible editor. */ + const activeGeneration = activeSection === 'schema' ? schemaGeneration : codeGeneration + const activeError = + activeSection === 'schema' ? schemaError : codeGeneration.isStreaming ? null : codeError - const hasChanges = useMemo(() => { - if (!isEditing) return true - return jsonSchema !== initialJsonSchema || functionCode !== initialFunctionCode - }, [isEditing, jsonSchema, initialJsonSchema, functionCode, initialFunctionCode]) + const isSchemaValid = useMemo(() => validateCustomToolSchema(jsonSchema).isValid, [jsonSchema]) - const hasUnsavedChanges = useMemo(() => { - if (isEditing) { - return jsonSchema !== initialJsonSchema || functionCode !== initialFunctionCode - } - return jsonSchema.trim().length > 0 || functionCode.trim().length > 0 - }, [isEditing, jsonSchema, initialJsonSchema, functionCode, initialFunctionCode]) + const edited = jsonSchema !== initialJsonSchema || functionCode !== initialFunctionCode + const canSave = !isEditing || edited + const hasUnsavedChanges = isEditing + ? edited + : jsonSchema.trim().length > 0 || functionCode.trim().length > 0 const handleCloseAttempt = () => { if (hasUnsavedChanges && !schemaGeneration.isStreaming && !codeGeneration.isStreaming) { @@ -438,7 +199,7 @@ try { return } - const { isValid, error } = validateSchema(jsonSchema) + const { isValid, error } = validateCustomToolSchema(jsonSchema) if (!isValid) { setSchemaError(error) setActiveSection('schema') @@ -509,9 +270,7 @@ try { const errorMessage = getErrorMessage(error, 'Failed to save custom tool') if (errorMessage.includes('Cannot change function name')) { - setSchemaError( - 'Function name cannot be changed after creation. To use a different name, delete this tool and create a new one.' - ) + setSchemaError(FUNCTION_NAME_LOCKED) } else { setSchemaError(errorMessage) } @@ -520,11 +279,10 @@ try { } const handleJsonSchemaChange = (value: string) => { - if (schemaGeneration.isLoading || schemaGeneration.isStreaming) return setJsonSchema(value) if (value.trim()) { - const { error } = validateSchema(value) + const { error } = validateCustomToolSchema(value) setSchemaError(error) } else { setSchemaError(null) @@ -532,265 +290,8 @@ try { } const handleFunctionCodeChange = (value: string) => { - if (codeGeneration.isLoading || codeGeneration.isStreaming) { - setFunctionCode(value) - if (codeError) { - setCodeError(null) - } - return - } - setFunctionCode(value) - if (codeError) { - setCodeError(null) - } - - const textarea = codeEditorRef.current?.querySelector('textarea') - if (textarea) { - const pos = textarea.selectionStart - setCursorPosition(pos) - - const textBeforeCursor = value.substring(0, pos) - const lines = textBeforeCursor.split('\n') - const currentLine = lines.length - const currentCol = lines[lines.length - 1].length - - try { - if (codeEditorRef.current) { - const editorRect = codeEditorRef.current.getBoundingClientRect() - const lineHeight = 21 - - const top = currentLine * lineHeight + 5 - const left = Math.min(currentCol * 8, editorRect.width - 260) - - setDropdownPosition({ top, left }) - } - } catch (error) { - logger.error('Error calculating cursor position:', { error }) - } - - const envVarTrigger = checkEnvVarTrigger(value, pos) - setShowEnvVars(envVarTrigger.show && !codeGeneration.isStreaming) - setSearchTerm(envVarTrigger.show ? envVarTrigger.searchTerm : '') - - const tagTrigger = checkTagTrigger(value, pos) - setShowTags(tagTrigger.show && !codeGeneration.isStreaming) - if (!tagTrigger.show) { - setActiveSourceBlockId(null) - } - - if (!codeGeneration.isStreaming && schemaParameters.length > 0) { - const schemaParamTrigger = checkSchemaParamTrigger(value, pos, schemaParameters) - if (schemaParamTrigger.show && !showSchemaParams) { - setShowSchemaParams(true) - setSchemaParamSelectedIndex(0) - } else if (!schemaParamTrigger.show && showSchemaParams) { - setShowSchemaParams(false) - } - } - } - } - - const checkSchemaParamTrigger = (text: string, cursorPos: number, parameters: any[]) => { - if (parameters.length === 0) return { show: false, searchTerm: '' } - - const beforeCursor = text.substring(0, cursorPos) - const words = beforeCursor.split(/[\s=();,{}[\]]+/) - const currentWord = words[words.length - 1] || '' - - if (currentWord.length > 0 && /^[a-zA-Z_][\w]*$/.test(currentWord)) { - const matchingParams = parameters.filter((param) => - param.name.toLowerCase().startsWith(currentWord.toLowerCase()) - ) - return { show: matchingParams.length > 0, searchTerm: currentWord, matches: matchingParams } - } - - return { show: false, searchTerm: '' } - } - - const handleEnvVarSelect = (newValue: string) => { - setFunctionCode(newValue) - setShowEnvVars(false) - } - - const handleTagSelect = (newValue: string) => { - setFunctionCode(newValue) - setShowTags(false) - setActiveSourceBlockId(null) - } - - const handleSchemaParamSelect = (paramName: string) => { - const textarea = codeEditorRef.current?.querySelector('textarea') - if (textarea) { - const pos = textarea.selectionStart - const beforeCursor = functionCode.substring(0, pos) - const afterCursor = functionCode.substring(pos) - - const words = beforeCursor.split(/[\s=();,{}[\]]+/) - const currentWord = words[words.length - 1] || '' - const wordStart = beforeCursor.lastIndexOf(currentWord) - - const newValue = beforeCursor.substring(0, wordStart) + paramName + afterCursor - setFunctionCode(newValue) - setShowSchemaParams(false) - - setTimeout(() => { - textarea.focus() - textarea.setSelectionRange(wordStart + paramName.length, wordStart + paramName.length) - }, 0) - } - } - - const handleKeyDown = (e: React.KeyboardEvent) => { - const isSchemaPromptVisible = activeSection === 'schema' && schemaGeneration.isPromptVisible - const isCodePromptVisible = activeSection === 'code' && codeGeneration.isPromptVisible - - if (e.key === 'Escape') { - if (isSchemaPromptVisible) { - schemaGeneration.hidePromptInline() - e.preventDefault() - e.stopPropagation() - return - } - if (isCodePromptVisible) { - codeGeneration.hidePromptInline() - e.preventDefault() - e.stopPropagation() - return - } - if (showEnvVars || showTags || showSchemaParams) { - setShowEnvVars(false) - setShowTags(false) - setShowSchemaParams(false) - e.preventDefault() - e.stopPropagation() - return - } - } - - if (activeSection === 'schema' && schemaGeneration.isStreaming) { - e.preventDefault() - return - } - if (activeSection === 'code' && codeGeneration.isStreaming) { - e.preventDefault() - return - } - - if (showSchemaParams && schemaParameters.length > 0) { - switch (e.key) { - case 'ArrowDown': - e.preventDefault() - e.stopPropagation() - setSchemaParamSelectedIndex((prev) => Math.min(prev + 1, schemaParameters.length - 1)) - return - case 'ArrowUp': - e.preventDefault() - e.stopPropagation() - setSchemaParamSelectedIndex((prev) => Math.max(prev - 1, 0)) - return - case 'Enter': - e.preventDefault() - e.stopPropagation() - if (schemaParamSelectedIndex >= 0 && schemaParamSelectedIndex < schemaParameters.length) { - const selectedParam = schemaParameters[schemaParamSelectedIndex] - handleSchemaParamSelect(selectedParam.name) - } - return - case 'Escape': - e.preventDefault() - e.stopPropagation() - setShowSchemaParams(false) - return - case ' ': - case 'Tab': - setShowSchemaParams(false) - return - } - } - - if (showEnvVars || showTags) { - if (['ArrowDown', 'ArrowUp', 'Enter'].includes(e.key)) { - e.preventDefault() - e.stopPropagation() - } - } - } - - const handleSchemaWandClick = () => { - if (schemaGeneration.isLoading || schemaGeneration.isStreaming) return - setIsSchemaPromptActive(true) - setSchemaPromptInput('') - setTimeout(() => { - schemaPromptInputRef.current?.focus() - }, 0) - } - - const handleSchemaPromptBlur = () => { - if (!schemaPromptInput.trim() && !schemaGeneration.isStreaming) { - setIsSchemaPromptActive(false) - } - } - - const handleSchemaPromptChange = (value: string) => { - setSchemaPromptInput(value) - } - - const handleSchemaPromptSubmit = () => { - const trimmedPrompt = schemaPromptInput.trim() - if (!trimmedPrompt || schemaGeneration.isLoading || schemaGeneration.isStreaming) return - schemaGeneration.generateStream({ prompt: trimmedPrompt }) - setSchemaPromptInput('') - setIsSchemaPromptActive(false) - } - - const handleSchemaPromptKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - e.preventDefault() - handleSchemaPromptSubmit() - } else if (e.key === 'Escape') { - e.preventDefault() - setSchemaPromptInput('') - setIsSchemaPromptActive(false) - } - } - - const handleCodeWandClick = () => { - if (codeGeneration.isLoading || codeGeneration.isStreaming) return - setIsCodePromptActive(true) - setCodePromptInput('') - setTimeout(() => { - codePromptInputRef.current?.focus() - }, 0) - } - - const handleCodePromptBlur = () => { - if (!codePromptInput.trim() && !codeGeneration.isStreaming) { - setIsCodePromptActive(false) - } - } - - const handleCodePromptChange = (value: string) => { - setCodePromptInput(value) - } - - const handleCodePromptSubmit = () => { - const trimmedPrompt = codePromptInput.trim() - if (!trimmedPrompt || codeGeneration.isLoading || codeGeneration.isStreaming) return - codeGeneration.generateStream({ prompt: trimmedPrompt }) - setCodePromptInput('') - setIsCodePromptActive(false) - } - - const handleCodePromptKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - e.preventDefault() - handleCodePromptSubmit() - } else if (e.key === 'Escape') { - e.preventDefault() - setCodePromptInput('') - setIsCodePromptActive(false) - } + if (codeError) setCodeError(null) } const handleDelete = async () => { @@ -805,16 +306,15 @@ try { }) logger.info(`Deleted tool: ${toolId}`) - if (onDelete) { - onDelete(toolId) - } + onDelete?.(toolId) handleClose() } catch (error) { logger.error('Error deleting custom tool:', error) - const errorMessage = getErrorMessage(error, 'Failed to delete custom tool') - setSchemaError(`${errorMessage}. Please try again.`) - setActiveSection('schema') + // A delete failure is not a schema problem — keep it out of the Schema slot. + toast.error("Couldn't delete tool", { + description: getErrorMessage(error, 'Please try again in a moment.'), + }) setShowDeleteConfirm(false) } } @@ -840,307 +340,47 @@ try { the caret as the body scrolls. */} - setActiveSection(value as ToolSection)} - /> - - {activeSection === 'schema' && ( -
-
-
- - {schemaError && ( -
- - {schemaError} -
- )} -
-
- {!isSchemaPromptActive ? ( - - ) : ( -
- handleSchemaPromptChange(e.target.value)} - onBlur={handleSchemaPromptBlur} - onKeyDown={handleSchemaPromptKeyDown} - disabled={schemaGeneration.isStreaming} - className={cn( - 'h-5 max-w-[200px] flex-1 text-xs', - schemaGeneration.isStreaming && 'text-muted-foreground' - )} - placeholder='Generate...' - /> - -
- )} -
-
- +
+ setActiveSection(value as ToolSection)} /> + {activeError && {activeError}}
+ {/* + Keyed by section so switching tabs resets the inline prompt — one + control drives both wands, and a half-typed Schema prompt must not + carry over and generate Code instead. + */} + activeGeneration.generateStream({ prompt })} + /> +
+ + {activeSection === 'schema' && ( + )} {activeSection === 'code' && ( -
-
-
- - {codeError && !codeGeneration.isStreaming && ( -
- - {codeError} -
- )} -
-
- {!isCodePromptActive ? ( - - ) : ( -
- handleCodePromptChange(e.target.value)} - onBlur={handleCodePromptBlur} - onKeyDown={handleCodePromptKeyDown} - disabled={codeGeneration.isStreaming} - className={cn( - 'h-5 max-w-[200px] flex-1 text-xs', - codeGeneration.isStreaming && 'text-muted-foreground' - )} - placeholder='Generate...' - /> - -
- )} -
-
- {schemaParameters.length > 0 && ( -
-
- - Available parameters: - - {schemaParameters.map((param) => ( - - {param.name} - - ))} - - Start typing a parameter name for autocomplete. - -
-
- )} -
- 0 ? '380px' : '420px'} - className={cn( - 'bg-[var(--bg)]', - codeError && !codeGeneration.isStreaming && 'border-[var(--text-error)]', - (codeGeneration.isLoading || codeGeneration.isStreaming) && - 'cursor-not-allowed opacity-50' - )} - gutterClassName='bg-[var(--bg)]' - highlightVariables={true} - disabled={codeGeneration.isLoading || codeGeneration.isStreaming} - onKeyDown={handleKeyDown} - schemaParameters={schemaParameters} - /> - - {showEnvVars && ( - { - setShowEnvVars(false) - setSearchTerm('') - }} - className='w-64' - style={{ - position: 'absolute', - top: `${dropdownPosition.top}px`, - left: `${dropdownPosition.left}px`, - }} - /> - )} - - {showTags && ( - { - setShowTags(false) - setActiveSourceBlockId(null) - }} - className='w-64' - style={{ - position: 'absolute', - top: `${dropdownPosition.top}px`, - left: `${dropdownPosition.left}px`, - }} - /> - )} - - {showSchemaParams && schemaParameters.length > 0 && ( - { - if (!open) { - setShowSchemaParams(false) - } - }} - colorScheme='inverted' - > - -
- - e.preventDefault()} - onCloseAutoFocus={(e) => e.preventDefault()} - > - - Available Parameters - {schemaParameters.map((param, index) => ( - setSchemaParamSelectedIndex(index)} - onMouseDown={(e) => { - e.preventDefault() - e.stopPropagation() - handleSchemaParamSelect(param.name) - }} - ref={(el) => { - if (el) { - schemaParamItemRefs.current.set(index, el) - } - }} - > - {param.name} - {param.type && param.type !== 'any' && ( - - {param.type} - - )} - - ))} - - - - )} -
-
+ )} @@ -1181,7 +421,7 @@ try { primaryAction={{ label: isEditing ? 'Update Tool' : 'Save Tool', onClick: handleSave, - disabled: !isSchemaValid || !!schemaError || !hasChanges, + disabled: !isSchemaValid || !!schemaError || !canSave, }} /> )} @@ -1192,13 +432,7 @@ try { onOpenChange={setShowDeleteConfirm} srTitle='Delete Custom Tool' title='Delete Custom Tool' - text={[ - { - text: 'This will permanently delete the tool and remove it from any workflows that are using it.', - error: true, - }, - ' This action cannot be undone.', - ]} + text={CUSTOM_TOOL_DELETE_CONFIRM_TEXT} confirm={{ label: 'Delete', onClick: handleDelete, diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-wand.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-wand.ts index 1ac9e042573..dc6bd65b33f 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-wand.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-wand.ts @@ -136,17 +136,6 @@ export function useWand({ setError(null) }, []) - const openPrompt = useCallback(() => { - setIsPromptVisible(true) - setPromptInputValue('') - }, []) - - const closePrompt = useCallback(() => { - if (isLoading) return - setIsPromptVisible(false) - setPromptInputValue('') - }, [isLoading]) - const generateStream = useCallback( async ({ prompt }: { prompt: string }) => { if (!prompt) { @@ -294,8 +283,6 @@ export function useWand({ generateStream, showPromptInline, hidePromptInline, - openPrompt, - closePrompt, updatePromptValue, cancelGeneration, } diff --git a/apps/sim/components/permissions/add-people-modal.tsx b/apps/sim/components/permissions/add-people-modal.tsx new file mode 100644 index 00000000000..dbfe6d129d6 --- /dev/null +++ b/apps/sim/components/permissions/add-people-modal.tsx @@ -0,0 +1,171 @@ +'use client' + +import { useCallback, useMemo, useState } from 'react' +import { + ChipModal, + ChipModalBody, + ChipModalError, + ChipModalField, + ChipModalFooter, + ChipModalHeader, +} from '@sim/emcn' +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { partitionSettledFailures, resolveAddEmail } from '@/lib/workspaces/sharing' +import { useWorkspacePermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' +import { MEMBER_ROLE_OPTIONS, type MemberRole } from './member-role-options' + +const logger = createLogger('AddPeopleModal') + +export interface AddPeopleTarget { + email: string + userId: string +} + +interface AddPeopleModalProps { + open: boolean + onOpenChange: (open: boolean) => void + /** Lowercased emails that already have access — rejected as duplicates. */ + existingMemberEmails: Set + /** Grants one person the role; a rejection surfaces as a partial failure. */ + addMember: (target: AddPeopleTarget, role: MemberRole) => Promise + /** + * Hides the Role field for resources without per-member roles (skills): + * every add is a plain grant and `addMember` receives the default role. + */ + hideRole?: boolean +} + +/** + * Shared "Add people" modal for member-managed resources (credentials, skills): + * grants existing workspace members access, optionally with a chosen role. + * Emails are validated against the workspace roster and current membership; + * each add is an idempotent upsert and partial failures keep only the people + * that still need adding. + */ +export function AddPeopleModal({ + open, + onOpenChange, + existingMemberEmails, + addMember, + hideRole = false, +}: AddPeopleModalProps) { + const { workspacePermissions } = useWorkspacePermissionsContext() + + const [emailsToAdd, setEmailsToAdd] = useState([]) + const [roleToAdd, setRoleToAdd] = useState('member') + const [isAdding, setIsAdding] = useState(false) + const [submitError, setSubmitError] = useState(null) + + const workspaceUserIdByEmail = useMemo( + () => + new Map( + (workspacePermissions?.users ?? []).map((user) => [user.email.toLowerCase(), user.userId]) + ), + [workspacePermissions?.users] + ) + + const validateAddEmail = useCallback( + (email: string): string | null => { + const result = resolveAddEmail(email, { workspaceUserIdByEmail, existingMemberEmails }) + return 'error' in result ? result.error : null + }, + [workspaceUserIdByEmail, existingMemberEmails] + ) + + const handleClose = useCallback(() => { + setEmailsToAdd([]) + setRoleToAdd('member') + setSubmitError(null) + onOpenChange(false) + }, [onOpenChange]) + + const handleAddPeople = useCallback(async () => { + if (emailsToAdd.length === 0 || isAdding) return + setSubmitError(null) + const targets = emailsToAdd + .map((email) => { + const result = resolveAddEmail(email, { workspaceUserIdByEmail, existingMemberEmails }) + return 'userId' in result ? { email, userId: result.userId } : null + }) + .filter((target): target is AddPeopleTarget => target !== null) + if (targets.length === 0) return + + setIsAdding(true) + try { + const results = await Promise.allSettled( + targets.map((target) => addMember(target, roleToAdd)) + ) + const failures = partitionSettledFailures(targets, results) + if (failures.length === 0) { + handleClose() + return + } + setEmailsToAdd(failures.map((target) => target.email)) + const firstError = results.find( + (result): result is PromiseRejectedResult => result.status === 'rejected' + ) + logger.error('Failed to add some members', firstError?.reason) + const reason = getErrorMessage(firstError?.reason, 'Please try again in a moment.') + setSubmitError( + failures.length === targets.length + ? `Couldn't add people. ${reason}` + : `Couldn't add ${failures.length} of ${targets.length} people. ${reason}` + ) + } finally { + setIsAdding(false) + } + }, [ + emailsToAdd, + isAdding, + workspaceUserIdByEmail, + existingMemberEmails, + roleToAdd, + addMember, + handleClose, + ]) + + return ( + { + if (!next) handleClose() + }} + srTitle='Add people' + > + Add people + + + {!hideRole && ( + setRoleToAdd(role as MemberRole)} + disabled={isAdding} + /> + )} + {submitError} + + + + ) +} diff --git a/apps/sim/components/permissions/index.ts b/apps/sim/components/permissions/index.ts index 69aff65d947..6fd107d31c1 100644 --- a/apps/sim/components/permissions/index.ts +++ b/apps/sim/components/permissions/index.ts @@ -1,3 +1,12 @@ +export { AddPeopleModal, type AddPeopleTarget } from './add-people-modal' +export { + MEMBER_ROLE_OPTIONS, + type MemberRole, + type MemberRoleOption, + SKILL_EDITOR_ROLE_OPTIONS, + type SkillEditorRole, +} from './member-role-options' +export { MemberRow, type MemberRowMember } from './member-row' export { type OrgRole, OrgRoleSelector, @@ -8,6 +17,7 @@ export { type CredentialRoleSource, credentialRoleLockReason, RoleLockTooltip, + skillEditorLockReason, type WorkspaceRoleSource, workspaceRoleLockReason, } from './role-lock' diff --git a/apps/sim/components/permissions/member-role-options.ts b/apps/sim/components/permissions/member-role-options.ts new file mode 100644 index 00000000000..321f91149e3 --- /dev/null +++ b/apps/sim/components/permissions/member-role-options.ts @@ -0,0 +1,28 @@ +/** Role assignable to a member of a shared resource (credential, skill). */ +export type MemberRole = 'member' | 'admin' + +export interface MemberRoleOption { + value: MemberRole + label: string +} + +/** + * Roles assignable to a resource member. Shared by every member-management + * surface (credential detail, skill members) so role choices never drift. + */ +export const MEMBER_ROLE_OPTIONS: readonly MemberRoleOption[] = [ + { value: 'member', label: 'Member' }, + { value: 'admin', label: 'Admin' }, +] as const + +export type SkillEditorRole = 'editor' + +/** + * Skill membership is binary — a user either edits the skill or does not, and + * `skill_member` has no role column. The single option keeps the roster's role + * control identical in shape to the credential surface's while being honest + * that there is nothing to switch between; every skill row renders it disabled. + */ +export const SKILL_EDITOR_ROLE_OPTIONS: readonly { value: SkillEditorRole; label: string }[] = [ + { value: 'editor', label: 'Editor' }, +] as const diff --git a/apps/sim/components/permissions/member-row.tsx b/apps/sim/components/permissions/member-row.tsx new file mode 100644 index 00000000000..8a235584204 --- /dev/null +++ b/apps/sim/components/permissions/member-row.tsx @@ -0,0 +1,90 @@ +'use client' + +import { Avatar, AvatarFallback, Chip, ChipDropdown, cn } from '@sim/emcn' +import { getUserColor } from '@/lib/workspaces/colors' +import type { MemberRole } from './member-role-options' +import { RoleLockTooltip } from './role-lock' + +export interface MemberRowMember { + userId: string + userName: string | null + userEmail: string | null + role: TRole +} + +interface MemberRowProps { + member: MemberRowMember + /** Why the role is fixed (derived access); null when editable. */ + lockReason: string | null + /** Whether the viewer can act on rows (shows the Remove column). */ + canManage: boolean + roleDisabled: boolean + removeDisabled: boolean + /** + * Role choices for the dropdown. A surface whose membership is binary + * (skills) passes its own single option and always sets `roleDisabled`. + */ + roleOptions: readonly { value: TRole; label: string }[] + /** Omitted on surfaces with nothing to switch between (the role stays disabled). */ + onRoleChange?: (role: TRole) => void + onRemove: () => void +} + +/** + * One member row of a shared-resource member list: avatar + identity, a role + * dropdown (wrapped in a lock tooltip when the role is derived), and a remove + * action for managers. Consumers own the policy (who is locked/disabled); this + * row owns the chrome. + */ +export function MemberRow({ + member, + lockReason, + canManage, + roleDisabled, + removeDisabled, + roleOptions, + onRoleChange, + onRemove, +}: MemberRowProps) { + return ( +
+
+ + + {(member.userName || member.userEmail || '?').charAt(0).toUpperCase()} + + +
+ + {member.userName || member.userEmail || member.userId} + + + {member.userEmail || member.userId} + +
+
+ + onRoleChange?.(role as TRole)} + /> + + {canManage && ( + + Remove + + )} +
+ ) +} diff --git a/apps/sim/components/permissions/role-lock.tsx b/apps/sim/components/permissions/role-lock.tsx index 6988b1200c4..db384516930 100644 --- a/apps/sim/components/permissions/role-lock.tsx +++ b/apps/sim/components/permissions/role-lock.tsx @@ -31,6 +31,15 @@ export function credentialRoleLockReason( return null } +/** + * Explanation shown when a skill editor's access is inherited from their + * workspace admin role rather than an explicit grant, and so cannot be removed. + * Returns null for explicitly added editors. + */ +export function skillEditorLockReason(isWorkspaceAdmin: boolean): string | null { + return isWorkspaceAdmin ? 'Workspace admins are automatically skill editors' : null +} + interface RoleLockTooltipProps { reason: string | null children: ReactNode diff --git a/apps/sim/ee/workspace-forking/lib/copy/copy-resources.test.ts b/apps/sim/ee/workspace-forking/lib/copy/copy-resources.test.ts index d3863d3fb6b..e7413a06236 100644 --- a/apps/sim/ee/workspace-forking/lib/copy/copy-resources.test.ts +++ b/apps/sim/ee/workspace-forking/lib/copy/copy-resources.test.ts @@ -545,14 +545,26 @@ describe('copyForkResourceContainers external MCP server copy', () => { }) describe('copyForkResourceContainers skill copy', () => { - function makeSkillTx(rows: Array>) { + /** Sequential tx mock: each select resolves the next queued row set (skill rows, then member rows). */ + function makeSkillTx(selects: Array>>) { + let call = 0 const inserted: Array> = [] const tx = { - select: () => ({ from: () => ({ where: () => Promise.resolve(rows) }) }), + select: () => { + const result = Promise.resolve(selects[call++] ?? []) + const chain = { + from: () => chain, + innerJoin: () => chain, + where: () => result, + } + return chain + }, insert: () => ({ values: (values: Array>) => { inserted.push(...values) - return Promise.resolve() + return Object.assign(Promise.resolve(), { + onConflictDoNothing: () => Promise.resolve(), + }) }, }), } @@ -568,20 +580,20 @@ describe('copyForkResourceContainers skill copy', () => { knowledgeBases: [], } + const sourceSkillRow = { + id: 'sk-1', + name: 'My Skill', + description: 'desc', + workspaceId: 'src-ws', + userId: 'src-user', + createdAt: new Date(), + updatedAt: new Date(), + } + it('copies the skill body IN-DB and carries only the child id in the content plan', async () => { // The source projection deliberately omits `content` (it is copied server-side), so the row // fed to the tx mock has none - the body must never be materialized in app memory here. - const { tx, inserted } = makeSkillTx([ - { - id: 'sk-1', - name: 'My Skill', - description: 'desc', - workspaceId: 'src-ws', - userId: 'src-user', - createdAt: new Date(), - updatedAt: new Date(), - }, - ]) + const { tx, inserted } = makeSkillTx([[sourceSkillRow], []]) const result = await copyForkResourceContainers({ tx, @@ -604,6 +616,37 @@ describe('copyForkResourceContainers skill copy', () => { expect(result.contentPlan.skills).toEqual([{ childId }]) expect(result.names.skills).toEqual(['My Skill']) }) + + it('copies editor grants onto the child skill for users in the target roster', async () => { + // The editor query joins the child-workspace permissions in-DB, so the + // mock's second row set already represents source editors ∩ target roster. + const { tx, inserted } = makeSkillTx([ + [sourceSkillRow], + [ + { skillId: 'sk-1', userId: 'editor-1' }, + { skillId: 'sk-1', userId: 'editor-2' }, + ], + ]) + + await copyForkResourceContainers({ + tx, + sourceWorkspaceId: 'src-ws', + childWorkspaceId: 'child-ws', + userId: 'user-1', + now: new Date(), + selection: skillSelection, + workflowIdMap: new Map(), + }) + + const childSkill = inserted[0] + const firstGrant = inserted[1] + expect(firstGrant.skillId).toBe(childSkill.id) + expect(firstGrant.userId).toBe('editor-1') + + const secondGrant = inserted[2] + expect(secondGrant.skillId).toBe(childSkill.id) + expect(secondGrant.userId).toBe('editor-2') + }) }) describe('copyForkResourceContainers knowledge-base tag definitions', () => { diff --git a/apps/sim/ee/workspace-forking/lib/copy/copy-resources.ts b/apps/sim/ee/workspace-forking/lib/copy/copy-resources.ts index 2c7eda693b5..63c32d56549 100644 --- a/apps/sim/ee/workspace-forking/lib/copy/copy-resources.ts +++ b/apps/sim/ee/workspace-forking/lib/copy/copy-resources.ts @@ -6,7 +6,9 @@ import { knowledgeBase, knowledgeBaseTagDefinitions, mcpServers, + permissions, skill, + skillMember, userTableDefinitions, userTableRows, workflowMcpServer, @@ -335,6 +337,7 @@ export async function copyForkResourceContainers( .from(skill) .where(and(inArray(skill.id, selection.skills), eq(skill.workspaceId, sourceWorkspaceId))) const inserts: SkillSkeletonInsert[] = [] + const childSkillIdBySource = new Map() for (const row of rows) { const childId = generateId() inserts.push({ @@ -348,11 +351,52 @@ export async function copyForkResourceContainers( createdAt: now, updatedAt: now, }) + childSkillIdBySource.set(row.id, childId) record('skill', row.id, childId) contentPlan.skills.push({ childId }) names.skills.push(row.name) } - if (inserts.length > 0) await tx.insert(skill).values(inserts) + if (inserts.length > 0) { + await tx.insert(skill).values(inserts) + + // Copy editor grants for users who are members of the child workspace. + // Workspace admins need no rows — they are derived editors in the child + // too (mirrors credential member propagation otherwise). + const memberRows = await tx + .select({ + skillId: skillMember.skillId, + userId: skillMember.userId, + }) + .from(skillMember) + .innerJoin( + permissions, + and( + eq(permissions.userId, skillMember.userId), + eq(permissions.entityType, 'workspace'), + eq(permissions.entityId, childWorkspaceId) + ) + ) + .where(inArray(skillMember.skillId, Array.from(childSkillIdBySource.keys()))) + const memberInserts = memberRows.flatMap((member) => { + const childSkillId = childSkillIdBySource.get(member.skillId) + if (!childSkillId) return [] + return [ + { + id: generateId(), + skillId: childSkillId, + userId: member.userId, + createdAt: now, + updatedAt: now, + }, + ] + }) + if (memberInserts.length > 0) { + await tx + .insert(skillMember) + .values(memberInserts) + .onConflictDoNothing({ target: [skillMember.skillId, skillMember.userId] }) + } + } } if (selection.mcpServers.length > 0) { diff --git a/apps/sim/executor/handlers/agent/skills-resolver.test.ts b/apps/sim/executor/handlers/agent/skills-resolver.test.ts index fb9103da2bc..bfc68245b28 100644 --- a/apps/sim/executor/handlers/agent/skills-resolver.test.ts +++ b/apps/sim/executor/handlers/agent/skills-resolver.test.ts @@ -3,20 +3,25 @@ */ import { dbChainMockFns, queueTableRows, resetDbChainMock, schemaMock } from '@sim/testing' import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' -import { resolveSkillContent } from './skills-resolver' +import { + resolveSkillContent, + resolveSkillContentById, + resolveSkillMetadata, +} from './skills-resolver' -// resolveSkillContent is the shared resolver invoked when a workflow agent block -// calls load_skill. -describe('resolveSkillContent', () => { - beforeEach(() => { - vi.clearAllMocks() - resetDbChainMock() - }) +// resolveSkillContent is the shared resolver invoked when a workflow agent +// block calls load_skill. Skill editors gate editing only — resolution never +// blocks on the acting user. +beforeEach(() => { + vi.clearAllMocks() + resetDbChainMock() +}) - afterAll(() => { - resetDbChainMock() - }) +afterAll(() => { + resetDbChainMock() +}) +describe('resolveSkillContent', () => { it('returns null without a skill name or workspace', async () => { expect(await resolveSkillContent('', 'ws-1')).toBeNull() expect(await resolveSkillContent('x', '')).toBeNull() @@ -29,7 +34,7 @@ describe('resolveSkillContent', () => { }) it('resolves a workspace user skill by name', async () => { - queueTableRows(schemaMock.skill, [{ content: '# Playbook', name: 'posthog-playbook' }]) + queueTableRows(schemaMock.skill, [{ content: '# Playbook' }]) expect(await resolveSkillContent('posthog-playbook', 'ws-1')).toBe('# Playbook') }) @@ -37,3 +42,30 @@ describe('resolveSkillContent', () => { expect(await resolveSkillContent('missing', 'ws-1')).toBeNull() }) }) + +describe('resolveSkillContentById', () => { + it('resolves a workspace skill by id', async () => { + queueTableRows(schemaMock.skill, [{ content: '# Body', name: 'my-skill' }]) + expect(await resolveSkillContentById('sk-1', 'ws-1')).toEqual({ + name: 'my-skill', + content: '# Body', + }) + }) + + it('returns null when the skill does not exist in the workspace', async () => { + expect(await resolveSkillContentById('missing', 'ws-1')).toBeNull() + }) +}) + +describe('resolveSkillMetadata', () => { + it('returns every attached skill in the workspace', async () => { + queueTableRows(schemaMock.skill, [ + { id: 'sk-1', name: 'a', description: 'A' }, + { id: 'sk-2', name: 'b', description: 'B' }, + ]) + + const metadata = await resolveSkillMetadata([{ skillId: 'sk-1' }, { skillId: 'sk-2' }], 'ws-1') + + expect(metadata.map((m) => m.name)).toEqual(['a', 'b']) + }) +}) diff --git a/apps/sim/executor/handlers/agent/skills-resolver.ts b/apps/sim/executor/handlers/agent/skills-resolver.ts index b5a145562ee..b14024ec570 100644 --- a/apps/sim/executor/handlers/agent/skills-resolver.ts +++ b/apps/sim/executor/handlers/agent/skills-resolver.ts @@ -45,11 +45,11 @@ export async function resolveSkillMetadata( try { const rows = await db - .select({ name: skill.name, description: skill.description }) + .select({ id: skill.id, name: skill.name, description: skill.description }) .from(skill) .where(and(eq(skill.workspaceId, workspaceId), inArray(skill.id, dbSkillIds))) - return [...metadata, ...rows] + return [...metadata, ...rows.map((row) => ({ name: row.name, description: row.description }))] } catch (error) { logger.error('Failed to resolve skill metadata', { error, dbSkillIds, workspaceId }) return metadata @@ -71,7 +71,7 @@ export async function resolveSkillContent( try { const rows = await db - .select({ content: skill.content, name: skill.name }) + .select({ content: skill.content }) .from(skill) .where(and(eq(skill.workspaceId, workspaceId), eq(skill.name, skillName))) .limit(1) @@ -109,7 +109,7 @@ export async function resolveSkillContentById( return null } - return rows[0] + return { name: rows[0].name, content: rows[0].content } } catch (error) { logger.error('Failed to resolve skill content', { error, skillId, workspaceId }) return null diff --git a/apps/sim/executor/handlers/pi/context.ts b/apps/sim/executor/handlers/pi/context.ts index dfabfd67a5c..b6e41e74e50 100644 --- a/apps/sim/executor/handlers/pi/context.ts +++ b/apps/sim/executor/handlers/pi/context.ts @@ -26,7 +26,9 @@ function isMemoryEnabled(config: PiMemoryConfig): boolean { return !!config.memoryType && config.memoryType !== 'none' } -/** Resolves selected skill inputs to full `{ name, content }` entries for Pi. */ +/** + * Resolves selected skill inputs to full `{ name, content }` entries for Pi. + */ export async function resolvePiSkills( skillInputs: unknown, workspaceId: string | undefined @@ -36,15 +38,8 @@ export async function resolvePiSkills( const skills: PiSkill[] = [] for (const input of skillInputs as SkillInput[]) { if (!input?.skillId) continue - try { - const resolved = await resolveSkillContentById(input.skillId, workspaceId) - if (resolved) skills.push({ name: resolved.name, content: resolved.content }) - } catch (error) { - logger.warn('Failed to resolve skill for Pi', { - skillId: input.skillId, - error: getErrorMessage(error), - }) - } + const resolved = await resolveSkillContentById(input.skillId, workspaceId) + if (resolved) skills.push({ name: resolved.name, content: resolved.content }) } return skills } diff --git a/apps/sim/executor/handlers/pi/pi-handler.ts b/apps/sim/executor/handlers/pi/pi-handler.ts index a46637c70bd..ebbaa2660d7 100644 --- a/apps/sim/executor/handlers/pi/pi-handler.ts +++ b/apps/sim/executor/handlers/pi/pi-handler.ts @@ -135,7 +135,6 @@ export class PiBlockHandler implements BlockHandler { } return this.runPi(ctx, block, runCloudReviewPi, params) } - const memoryConfig: PiMemoryConfig = { memoryType: asOptString(inputs.memoryType) as PiMemoryConfig['memoryType'], conversationId: asOptString(inputs.conversationId), diff --git a/apps/sim/hooks/queries/skills.ts b/apps/sim/hooks/queries/skills.ts index e2bd5a1265f..6817f6b6249 100644 --- a/apps/sim/hooks/queries/skills.ts +++ b/apps/sim/hooks/queries/skills.ts @@ -3,14 +3,19 @@ import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tansta import { requestJson } from '@/lib/api/client/request' import { deleteSkillContract, + listSkillMembersContract, listSkillsContract, + removeSkillMemberContract, type Skill, + type SkillEditor, + upsertSkillMemberContract, upsertSkillsContract, } from '@/lib/api/contracts' const logger = createLogger('SkillsQueries') export const SKILL_LIST_STALE_TIME = 60 * 1000 +export const SKILL_MEMBER_LIST_STALE_TIME = 30 * 1000 export type SkillDefinition = Skill @@ -21,6 +26,7 @@ export const skillsKeys = { all: ['skills'] as const, lists: () => [...skillsKeys.all, 'list'] as const, list: (workspaceId: string) => [...skillsKeys.lists(), workspaceId] as const, + members: (skillId?: string) => [...skillsKeys.all, 'members', skillId ?? ''] as const, } /** @@ -70,7 +76,13 @@ export function useCreateSkill() { const { data } = await requestJson(upsertSkillsContract, { body: { - skills: [{ name: s.name, description: s.description, content: s.content }], + skills: [ + { + name: s.name, + description: s.description, + content: s.content, + }, + ], workspaceId, }, }) @@ -114,25 +126,11 @@ export function useUpdateSkill() { mutationFn: async ({ workspaceId, skillId, updates }: UpdateSkillParams) => { logger.info(`Updating skill: ${skillId} in workspace ${workspaceId}`) - const currentSkills = queryClient.getQueryData( - skillsKeys.list(workspaceId) - ) - const currentSkill = currentSkills?.find((s) => s.id === skillId) - - if (!currentSkill) { - throw new Error('Skill not found') - } - + // Updates are partial on the wire — omitted fields are preserved + // server-side, so nothing is re-sent from a possibly-stale cache. const { data } = await requestJson(upsertSkillsContract, { body: { - skills: [ - { - id: skillId, - name: updates.name ?? currentSkill.name, - description: updates.description ?? currentSkill.description, - content: updates.content ?? currentSkill.content, - }, - ], + skills: [{ id: skillId, ...updates }], workspaceId, }, }) @@ -172,6 +170,7 @@ export function useUpdateSkill() { }, onSettled: (_data, _error, variables) => { queryClient.invalidateQueries({ queryKey: skillsKeys.list(variables.workspaceId) }) + queryClient.invalidateQueries({ queryKey: skillsKeys.members(variables.skillId) }) }, }) } @@ -224,3 +223,88 @@ export function useDeleteSkill() { }, }) } + +/** + * Fetch the editor roster for a skill (explicit editors plus derived workspace + * admins). Built-in skills have no editors — callers should not enable this + * for readOnly skills. + */ +export function useSkillMembers(skillId?: string, options?: { enabled?: boolean }) { + return useQuery({ + queryKey: skillsKeys.members(skillId), + queryFn: async ({ signal }) => { + if (!skillId) return [] + const data = await requestJson(listSkillMembersContract, { + params: { id: skillId }, + signal, + }) + return data.editors + }, + enabled: Boolean(skillId) && (options?.enabled ?? true), + staleTime: SKILL_MEMBER_LIST_STALE_TIME, + }) +} + +interface UpsertSkillMemberParams { + skillId: string + workspaceId: string + userId: string +} + +export function useUpsertSkillMember() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async ({ skillId, userId }: UpsertSkillMemberParams) => { + return requestJson(upsertSkillMemberContract, { + params: { id: skillId }, + body: { userId }, + }) + }, + onSuccess: (_data, variables) => { + queryClient.invalidateQueries({ queryKey: skillsKeys.members(variables.skillId) }) + queryClient.invalidateQueries({ queryKey: skillsKeys.list(variables.workspaceId) }) + }, + }) +} + +interface RemoveSkillMemberParams { + skillId: string + workspaceId: string + userId: string +} + +export function useRemoveSkillMember() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async ({ skillId, userId }: RemoveSkillMemberParams) => { + return requestJson(removeSkillMemberContract, { + params: { id: skillId }, + query: { userId }, + }) + }, + onMutate: async (variables) => { + await queryClient.cancelQueries({ queryKey: skillsKeys.members(variables.skillId) }) + const previousEditors = queryClient.getQueryData( + skillsKeys.members(variables.skillId) + ) + if (previousEditors) { + queryClient.setQueryData( + skillsKeys.members(variables.skillId), + previousEditors.filter((editor) => editor.userId !== variables.userId) + ) + } + return { previousEditors } + }, + onError: (_err, variables, context) => { + if (context?.previousEditors) { + queryClient.setQueryData(skillsKeys.members(variables.skillId), context.previousEditors) + } + }, + onSettled: (_data, _error, variables) => { + queryClient.invalidateQueries({ queryKey: skillsKeys.members(variables.skillId) }) + queryClient.invalidateQueries({ queryKey: skillsKeys.list(variables.workspaceId) }) + }, + }) +} diff --git a/apps/sim/lib/api/contracts/skills.ts b/apps/sim/lib/api/contracts/skills.ts index 950acf782e1..31af51cd489 100644 --- a/apps/sim/lib/api/contracts/skills.ts +++ b/apps/sim/lib/api/contracts/skills.ts @@ -8,6 +8,11 @@ export const skillSchema = z.object({ name: z.string(), description: z.string(), content: z.string(), + /** + * Whether the caller can edit, delete, and share the skill (explicit editor + * or derived workspace admin). Always false for built-in template skills. + */ + canEdit: z.boolean(), createdAt: z.string(), updatedAt: z.string(), /** True for built-in template skills, which are read-only and not stored in the DB. */ @@ -16,17 +21,57 @@ export const skillSchema = z.object({ export type Skill = z.output -export const skillUpsertItemSchema = z.object({ - id: z.string().optional(), - name: z - .string() - .min(1, 'Skill name is required') - .max(64) - .regex(/^[a-z0-9]+(-[a-z0-9]+)*$/, 'Name must be kebab-case (e.g. my-skill)'), - description: z.string().min(1, 'Description is required').max(1024), - content: z.string().min(1, 'Content is required').max(50_000, 'Content is too large'), +/** + * One entry of a skill's editor roster: a workspace admin (derived, always an + * editor) or an explicitly added editor. + */ +export const skillEditorSchema = z.object({ + id: z.string(), + userId: z.string(), + userName: z.string().nullable(), + userEmail: z.string().nullable(), + userImage: z.string().nullable().optional(), + isWorkspaceAdmin: z.boolean(), }) +export type SkillEditor = z.output + +const skillNameSchema = z + .string() + .min(1, 'Skill name is required') + .max(64) + .regex(/^[a-z0-9]+(-[a-z0-9]+)*$/, 'Name must be kebab-case (e.g. my-skill)') +const skillDescriptionSchema = z.string().min(1, 'Description is required').max(1024) +const skillContentSchema = z + .string() + .min(1, 'Content is required') + .max(50_000, 'Content is too large') + +/** + * One skill in an upsert. Creates (no `id`) require name/description/content; + * updates (`id` set) are partial — omitted fields keep their current values + * server-side, so a partial edit can never clobber a concurrent content edit. + */ +export const skillUpsertItemSchema = z + .object({ + id: z.string().min(1).optional(), + name: skillNameSchema.optional(), + description: skillDescriptionSchema.optional(), + content: skillContentSchema.optional(), + }) + .superRefine((item, ctx) => { + if (item.id) return + if (item.name === undefined) { + ctx.addIssue({ code: 'custom', path: ['name'], message: 'Skill name is required' }) + } + if (item.description === undefined) { + ctx.addIssue({ code: 'custom', path: ['description'], message: 'Description is required' }) + } + if (item.content === undefined) { + ctx.addIssue({ code: 'custom', path: ['content'], message: 'Content is required' }) + } + }) + export const listSkillsQuerySchema = z.object({ workspaceId: z.string().min(1), }) @@ -43,10 +88,6 @@ export const deleteSkillQuerySchema = z.object({ source: z.enum(['settings', 'tool_input']).optional(), }) -export const importSkillBodySchema = z.object({ - url: z.string().url('A valid URL is required'), -}) - export const listSkillsContract = defineRouteContract({ method: 'GET', path: '/api/skills', @@ -84,14 +125,54 @@ export const deleteSkillContract = defineRouteContract({ }, }) -export const importSkillContract = defineRouteContract({ +export const skillIdParamsSchema = z.object({ + id: z.string().min(1, 'Skill id is required'), +}) + +export const upsertSkillMemberBodySchema = z.object({ + userId: z.string().min(1, 'User id is required'), +}) + +export type UpsertSkillMemberBody = z.input + +export const removeSkillMemberQuerySchema = z.object({ + userId: z.string().min(1, 'User id is required'), +}) + +export const listSkillMembersContract = defineRouteContract({ + method: 'GET', + path: '/api/skills/[id]/members', + params: skillIdParamsSchema, + response: { + mode: 'json', + schema: z.object({ + editors: z.array(skillEditorSchema), + }), + }, +}) + +export const upsertSkillMemberContract = defineRouteContract({ method: 'POST', - path: '/api/skills/import', - body: importSkillBodySchema, + path: '/api/skills/[id]/members', + params: skillIdParamsSchema, + body: upsertSkillMemberBodySchema, response: { mode: 'json', schema: z.object({ - content: z.string(), + success: z.literal(true), + }), + }, +}) + +export const removeSkillMemberContract = defineRouteContract({ + method: 'DELETE', + path: '/api/skills/[id]/members', + params: skillIdParamsSchema, + query: removeSkillMemberQuerySchema, + response: { + mode: 'json', + schema: z.object({ + success: z.literal(true), }), }, }) diff --git a/apps/sim/lib/billing/organizations/membership.ts b/apps/sim/lib/billing/organizations/membership.ts index 7d2955ef587..3cb64bedefc 100644 --- a/apps/sim/lib/billing/organizations/membership.ts +++ b/apps/sim/lib/billing/organizations/membership.ts @@ -47,6 +47,7 @@ import { enqueueOutboxEvent } from '@/lib/core/outbox/service' import { revokeWorkspaceCredentialMembershipsTx } from '@/lib/credentials/access' import type { DbOrTx } from '@/lib/db/types' import { acquireInvitationMutationLocks } from '@/lib/invitations/locks' +import { removeWorkspaceSkillMembershipsTx } from '@/lib/skills/access' import { reassignWorkflowOwnershipForWorkspaceMemberRemovalTx, WorkspaceBillingAccountRemovalError, @@ -1470,6 +1471,7 @@ export async function transferUserBetweenOrganizations( workspaceIds, params.userId ) + await removeWorkspaceSkillMembershipsTx(tx, workspaceIds, params.userId) } const [stats] = await tx @@ -1721,6 +1723,7 @@ export async function removeUserFromOrganization( workspaceIds, userId ) + await removeWorkspaceSkillMembershipsTx(tx, workspaceIds, userId) const capturedUsage = await captureDepartedUsage() return { @@ -1935,6 +1938,7 @@ export async function removeExternalUserFromOrganizationWorkspaces(params: { workspaceIds, userId ) + await removeWorkspaceSkillMembershipsTx(tx, workspaceIds, userId) return { workspaceAccessRevoked: deletedPermissions.length, diff --git a/apps/sim/lib/copilot/chat/workspace-context.ts b/apps/sim/lib/copilot/chat/workspace-context.ts index 11ee6ac3702..5276e3257f6 100644 --- a/apps/sim/lib/copilot/chat/workspace-context.ts +++ b/apps/sim/lib/copilot/chat/workspace-context.ts @@ -26,11 +26,11 @@ import { import { listWorkspaceFiles } from '@/lib/uploads/contexts/workspace' import { listCustomBlockSummariesForWorkspace } from '@/lib/workflows/custom-blocks/operations' import { listCustomTools } from '@/lib/workflows/custom-tools/operations' -import { listSkills } from '@/lib/workflows/skills/operations' +import { listSkillsForUser } from '@/lib/workflows/skills/operations' import { assertActiveWorkspaceAccess, getUsersWithPermissions, - getWorkspaceWithOwner, + type WorkspaceAccess, } from '@/lib/workspaces/permissions/utils' const logger = createLogger('WorkspaceContext') @@ -334,11 +334,17 @@ export function buildWorkspaceContextMd(data: WorkspaceMdData): string { // workspace is unavailable or a fetch fails. async function buildWorkspaceMdData( workspaceId: string, - userId: string + userId: string, + options?: { workspaceAccess?: WorkspaceAccess } ): Promise { try { - await assertActiveWorkspaceAccess(workspaceId, userId) - const wsRow = await getWorkspaceWithOwner(workspaceId) + // Reuse the caller's already-asserted access when provided (hot chat path); + // the id match keeps a mismatched cache from authorizing this workspace. + const workspaceAccess = + options?.workspaceAccess && options.workspaceAccess.workspace?.id === workspaceId + ? options.workspaceAccess + : await assertActiveWorkspaceAccess(workspaceId, userId) + const wsRow = workspaceAccess.hasAccess ? workspaceAccess.workspace : null if (!wsRow) { return null } @@ -421,7 +427,7 @@ async function buildWorkspaceMdData( .from(mcpServers) .where(and(eq(mcpServers.workspaceId, workspaceId), isNull(mcpServers.deletedAt))), - listSkills({ workspaceId, includeBuiltins: false }), + listSkillsForUser({ workspaceId, userId, includeBuiltins: false, workspaceAccess }), db .select({ @@ -555,9 +561,10 @@ const WORKSPACE_CONTEXT_UNAVAILABLE_MD = */ export async function generateWorkspaceContext( workspaceId: string, - userId: string + userId: string, + options?: { workspaceAccess?: WorkspaceAccess } ): Promise { - const data = await buildWorkspaceMdData(workspaceId, userId) + const data = await buildWorkspaceMdData(workspaceId, userId, options) return data ? buildWorkspaceMd(data) : WORKSPACE_CONTEXT_UNAVAILABLE_MD } diff --git a/apps/sim/lib/copilot/tools/handlers/management/manage-skill.ts b/apps/sim/lib/copilot/tools/handlers/management/manage-skill.ts index dc551e382e3..e3c1c7bfebc 100644 --- a/apps/sim/lib/copilot/tools/handlers/management/manage-skill.ts +++ b/apps/sim/lib/copilot/tools/handlers/management/manage-skill.ts @@ -3,7 +3,9 @@ import { createLogger } from '@sim/logger' import { getErrorMessage, toError } from '@sim/utils/errors' import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/request/types' import { captureServerEvent } from '@/lib/posthog/server' -import { deleteSkill, listSkills, upsertSkills } from '@/lib/workflows/skills/operations' +import { getSkillActorContext } from '@/lib/skills/access' +import { isBuiltinSkillId } from '@/lib/workflows/skills/builtin-skills' +import { deleteSkill, listSkillsForUser, upsertSkills } from '@/lib/workflows/skills/operations' const logger = createLogger('CopilotToolExecutor') @@ -33,22 +35,23 @@ export async function executeManageSkill( return { success: false, error: 'workspaceId is required' } } - const writeOps: string[] = ['add', 'edit', 'delete'] + // Workspace write gates only creation; edits and deletes are gated per skill + // below (skill editor — explicit editor row or derived workspace admin). if ( - writeOps.includes(operation) && + operation === 'add' && context.userPermission && context.userPermission !== 'write' && context.userPermission !== 'admin' ) { return { success: false, - error: `Permission denied: '${operation}' on manage_skill requires write access. You have '${context.userPermission}' permission.`, + error: `Permission denied: 'add' on manage_skill requires write access. You have '${context.userPermission}' permission.`, } } try { if (operation === 'list') { - const skills = await listSkills({ workspaceId }) + const skills = await listSkillsForUser({ workspaceId, userId: context.userId }) return { success: true, @@ -128,26 +131,36 @@ export async function executeManageSkill( } } - const existing = await listSkills({ workspaceId }) - const found = existing.find((s) => s.id === params.skillId) - if (!found) { + if (isBuiltinSkillId(params.skillId)) { + return { success: false, error: 'Built-in skills are read-only and cannot be modified' } + } + + const actor = await getSkillActorContext(params.skillId, context.userId) + if (!actor.skill || actor.skill.workspaceId !== workspaceId || !actor.hasWorkspaceAccess) { return { success: false, error: `Skill not found: ${params.skillId}` } } + if (!actor.canEdit) { + return { + success: false, + error: `Permission denied: editing skill "${actor.skill.name}" requires skill editor access. Ask a skill editor to add you.`, + } + } + // Partial update: omitted fields keep their current values server-side. await upsertSkills({ skills: [ { id: params.skillId, - name: params.name || found.name, - description: params.description || found.description, - content: params.content || found.content, + ...(params.name ? { name: params.name } : {}), + ...(params.description ? { description: params.description } : {}), + ...(params.content ? { content: params.content } : {}), }, ], workspaceId, userId: context.userId, }) - const updatedName = params.name || found.name + const updatedName = params.name || actor.skill.name recordAudit({ workspaceId, actorId: context.userId, @@ -176,8 +189,8 @@ export async function executeManageSkill( success: true, operation, skillId: params.skillId, - name: params.name || found.name, - message: `Updated skill "${params.name || found.name}"`, + name: updatedName, + message: `Updated skill "${updatedName}"`, }, } } @@ -187,6 +200,19 @@ export async function executeManageSkill( return { success: false, error: "'skillId' is required for 'delete'" } } + if (!isBuiltinSkillId(params.skillId)) { + const actor = await getSkillActorContext(params.skillId, context.userId) + if (!actor.skill || actor.skill.workspaceId !== workspaceId || !actor.hasWorkspaceAccess) { + return { success: false, error: `Skill not found: ${params.skillId}` } + } + if (!actor.canEdit) { + return { + success: false, + error: `Permission denied: deleting skill "${actor.skill.name}" requires skill editor access. Ask a skill editor to add you.`, + } + } + } + const deleted = await deleteSkill({ skillId: params.skillId, workspaceId }) if (!deleted) { return { success: false, error: `Skill not found: ${params.skillId}` } diff --git a/apps/sim/lib/copilot/vfs/workspace-vfs.ts b/apps/sim/lib/copilot/vfs/workspace-vfs.ts index cf3d48816e8..eeb6516f9f5 100644 --- a/apps/sim/lib/copilot/vfs/workspace-vfs.ts +++ b/apps/sim/lib/copilot/vfs/workspace-vfs.ts @@ -2154,9 +2154,11 @@ export class WorkspaceVFS { } /** - * Advertise workspace skills in the VFS without eagerly loading their bodies. - * Paths are registered as lazy so glob/WORKSPACE.md see them, but full content - * is fetched only when read (or a grep whose scope touches the path) resolves them. + * Advertise the workspace skills in the VFS without eagerly loading their + * bodies. Paths are registered as lazy so glob/WORKSPACE.md see them, but + * full content is fetched only when read (or a grep whose scope touches the + * path) resolves them. Skills are workspace-visible — everyone with + * workspace access sees and uses every skill. */ private async materializeSkills( workspaceId: string diff --git a/apps/sim/lib/credentials/access.test.ts b/apps/sim/lib/credentials/access.test.ts index 0fc2c8dc2f8..b5ae09c1418 100644 --- a/apps/sim/lib/credentials/access.test.ts +++ b/apps/sim/lib/credentials/access.test.ts @@ -8,6 +8,11 @@ const { mockCheckWorkspaceAccess } = vi.hoisted(() => ({ vi.mock('@/lib/workspaces/permissions/utils', () => ({ checkWorkspaceAccess: mockCheckWorkspaceAccess, + resolveWorkspaceAccess: vi.fn(async (workspaceId: string, userId: string, provided?: any) => + provided && provided.workspace?.id === workspaceId + ? provided + : mockCheckWorkspaceAccess(workspaceId, userId) + ), })) import { getCredentialActorContext } from '@/lib/credentials/access' diff --git a/apps/sim/lib/credentials/access.ts b/apps/sim/lib/credentials/access.ts index 1fd5728982a..005a47fe65d 100644 --- a/apps/sim/lib/credentials/access.ts +++ b/apps/sim/lib/credentials/access.ts @@ -2,7 +2,7 @@ import { db } from '@sim/db' import { credential, credentialMember, credentialTypeEnum } from '@sim/db/schema' import { and, eq, inArray } from 'drizzle-orm' import type { DbOrTx } from '@/lib/db/types' -import { checkWorkspaceAccess, type WorkspaceAccess } from '@/lib/workspaces/permissions/utils' +import { resolveWorkspaceAccess, type WorkspaceAccess } from '@/lib/workspaces/permissions/utils' type ActiveCredentialMember = typeof credentialMember.$inferSelect type CredentialRecord = typeof credential.$inferSelect @@ -74,11 +74,11 @@ export async function getCredentialActorContext( } } - const providedAccess = options?.workspaceAccess - const workspaceAccess = - providedAccess && providedAccess.workspace?.id === credentialRow.workspaceId - ? providedAccess - : await checkWorkspaceAccess(credentialRow.workspaceId, userId) + const workspaceAccess = await resolveWorkspaceAccess( + credentialRow.workspaceId, + userId, + options?.workspaceAccess + ) const [memberRow] = await db .select() .from(credentialMember) @@ -111,6 +111,13 @@ export async function getCredentialActorContext( * Workspace owners and admins are derived credential admins, so no per-credential * owner promotion is needed to avoid orphaning a credential. Returns the number * of memberships revoked. + * + * Deliberately DIVERGES from the skill sibling (`removeWorkspaceSkillMembershipsTx` + * deletes active rows): credential access is explicit-rows-only, so a revoked + * row here is an inert tombstone the env-credential join sync uses to avoid + * resurrecting access. Skills have an implicit workspace-shared grant, where a + * revoked row is a live per-skill DENY marker and a kept-active row would + * re-grant on rejoin — do not "harmonize" either helper toward the other. */ export async function revokeWorkspaceCredentialMembershipsTx( tx: DbOrTx, diff --git a/apps/sim/lib/mothership/inbox/executor.ts b/apps/sim/lib/mothership/inbox/executor.ts index b04b5655fe4..21fcc48c286 100644 --- a/apps/sim/lib/mothership/inbox/executor.ts +++ b/apps/sim/lib/mothership/inbox/executor.ts @@ -25,7 +25,7 @@ import { sendInboxResponse } from '@/lib/mothership/inbox/response' import type { AgentMailAttachment } from '@/lib/mothership/inbox/types' import { uploadFile } from '@/lib/uploads/core/storage-service' import { createFileContent, type MessageContent } from '@/lib/uploads/utils/file-utils' -import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' +import { checkWorkspaceAccess, getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' import { getWorkspaceBilledAccountUserId } from '@/lib/workspaces/utils' const logger = createLogger('InboxExecutor') @@ -210,21 +210,16 @@ export async function executeInboxTask(taskId: string): Promise { return { attachments, ...downloaded } } - const [ - attachmentResult, - workspaceContext, - integrationTools, - userPermission, - billingAttribution, - entitlements, - ] = await Promise.all([ - fetchAttachments(), - generateWorkspaceContext(ws.id, userId), - buildIntegrationToolSchemas(userId, undefined, undefined, ws.id), - getUserEntityPermissions(userId, 'workspace', ws.id).catch(() => null), - resolveBillingAttribution({ actorUserId: userId, workspaceId: ws.id }), - computeWorkspaceEntitlements(ws.id, userId), - ]) + const workspaceAccess = await checkWorkspaceAccess(ws.id, userId) + const userPermission = workspaceAccess.permission + const [attachmentResult, workspaceContext, integrationTools, billingAttribution, entitlements] = + await Promise.all([ + fetchAttachments(), + generateWorkspaceContext(ws.id, userId, { workspaceAccess }), + buildIntegrationToolSchemas(userId, undefined, undefined, ws.id), + resolveBillingAttribution({ actorUserId: userId, workspaceId: ws.id }), + computeWorkspaceEntitlements(ws.id, userId), + ]) const { attachments, fileAttachments, storedAttachments } = attachmentResult const truncatedTask = { diff --git a/apps/sim/lib/posthog/events.ts b/apps/sim/lib/posthog/events.ts index 61534aeb1a3..723717d1d36 100644 --- a/apps/sim/lib/posthog/events.ts +++ b/apps/sim/lib/posthog/events.ts @@ -213,6 +213,16 @@ export interface PostHogEventMap { source?: 'settings' | 'tool_input' } + skill_shared: { + skill_id: string + workspace_id: string + } + + skill_unshared: { + skill_id: string + workspace_id: string + } + workspace_deleted: { workspace_id: string workflow_count: number diff --git a/apps/sim/lib/skills/access.test.ts b/apps/sim/lib/skills/access.test.ts new file mode 100644 index 00000000000..d993edec97f --- /dev/null +++ b/apps/sim/lib/skills/access.test.ts @@ -0,0 +1,282 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockCheckWorkspaceAccess, mockGetUsersWithPermissions, dbState, makeChain, dbMock } = + vi.hoisted(() => { + const state = { results: [] as unknown[][] } + const chainFactory = () => { + const resolve = () => Promise.resolve(state.results.shift() ?? []) + const chain: any = {} + chain.from = vi.fn(() => chain) + chain.innerJoin = vi.fn(() => chain) + chain.where = vi.fn(() => chain) + chain.set = vi.fn(() => chain) + chain.limit = vi.fn(() => resolve()) + chain.returning = vi.fn(() => resolve()) + chain.then = (onFulfilled: any, onRejected: any) => resolve().then(onFulfilled, onRejected) + return chain + } + return { + mockCheckWorkspaceAccess: vi.fn(), + mockGetUsersWithPermissions: vi.fn(), + dbState: state, + makeChain: chainFactory, + dbMock: { + select: vi.fn(() => chainFactory()), + update: vi.fn(() => chainFactory()), + }, + } + }) + +vi.mock('@sim/db', () => ({ + db: dbMock, +})) + +vi.mock('@sim/db/schema', () => ({ + skill: { + id: 'skill.id', + workspaceId: 'skill.workspaceId', + name: 'skill.name', + }, + skillMember: { + id: 'skillMember.id', + skillId: 'skillMember.skillId', + userId: 'skillMember.userId', + }, +})) + +vi.mock('drizzle-orm', () => ({ + and: vi.fn((...args: unknown[]) => ({ and: args })), + eq: vi.fn((a: unknown, b: unknown) => ({ eq: [a, b] })), + inArray: vi.fn((a: unknown, b: unknown) => ({ inArray: [a, b] })), +})) + +vi.mock('@/lib/workspaces/permissions/utils', () => ({ + checkWorkspaceAccess: mockCheckWorkspaceAccess, + getUsersWithPermissions: mockGetUsersWithPermissions, + resolveWorkspaceAccess: vi.fn(async (workspaceId: string, userId: string, provided?: any) => + provided && provided.workspace?.id === workspaceId + ? provided + : mockCheckWorkspaceAccess(workspaceId, userId) + ), +})) + +import { + checkSkillsUpdateAccess, + getEditableSkillIds, + getSkillActorContext, + listSkillEditors, + removeWorkspaceSkillMembershipsTx, +} from '@/lib/skills/access' + +const wsAdmin = { hasAccess: true, canWrite: true, canAdmin: true, workspace: { id: 'ws' } } +const wsWrite = { hasAccess: true, canWrite: true, canAdmin: false, workspace: { id: 'ws' } } +const wsRead = { hasAccess: true, canWrite: false, canAdmin: false, workspace: { id: 'ws' } } +const wsNone = { hasAccess: false, canWrite: false, canAdmin: false, workspace: null } + +beforeEach(() => { + vi.clearAllMocks() + dbState.results = [] + mockGetUsersWithPermissions.mockResolvedValue([]) +}) + +describe('getSkillActorContext', () => { + it('grants edit access from an explicit editor row', async () => { + dbState.results = [[{ id: 's1', workspaceId: 'ws' }], [{ id: 'row-1' }]] + mockCheckWorkspaceAccess.mockResolvedValue(wsRead) + + const ctx = await getSkillActorContext('s1', 'user1') + + expect(ctx.hasWorkspaceAccess).toBe(true) + expect(ctx.canEdit).toBe(true) + }) + + it('derives edit access from workspace admin without any rows', async () => { + dbState.results = [[{ id: 's1', workspaceId: 'ws' }], []] + mockCheckWorkspaceAccess.mockResolvedValue(wsAdmin) + + const ctx = await getSkillActorContext('s1', 'admin-user') + + expect(ctx.canEdit).toBe(true) + }) + + it('lets rowless workspace members see but not edit', async () => { + dbState.results = [[{ id: 's1', workspaceId: 'ws' }], []] + mockCheckWorkspaceAccess.mockResolvedValue(wsWrite) + + const ctx = await getSkillActorContext('s1', 'writer') + + expect(ctx.hasWorkspaceAccess).toBe(true) + expect(ctx.canEdit).toBe(false) + }) + + it('denies everything without workspace access, even with an editor row', async () => { + dbState.results = [[{ id: 's1', workspaceId: 'ws' }], [{ id: 'row-1' }]] + mockCheckWorkspaceAccess.mockResolvedValue(wsNone) + + const ctx = await getSkillActorContext('s1', 'outsider') + + expect(ctx.hasWorkspaceAccess).toBe(false) + expect(ctx.canEdit).toBe(false) + }) + + it('returns an empty context when the skill does not exist', async () => { + dbState.results = [[]] + + const ctx = await getSkillActorContext('missing', 'user1') + + expect(ctx.skill).toBeNull() + expect(ctx.hasWorkspaceAccess).toBe(false) + expect(ctx.canEdit).toBe(false) + expect(mockCheckWorkspaceAccess).not.toHaveBeenCalled() + }) +}) + +describe('getEditableSkillIds', () => { + it('collects the editor rows from one workspace-scoped scan', async () => { + dbState.results = [[{ skillId: 's-mine' }, { skillId: 's-also-mine' }]] + mockCheckWorkspaceAccess.mockResolvedValue(wsRead) + + const access = await getEditableSkillIds('ws', 'user1') + + expect(access.canAdminWorkspace).toBe(false) + expect(access.editorSkillIds).toEqual(new Set(['s-mine', 's-also-mine'])) + }) + + it('flags workspace admins as editors of everything', async () => { + dbState.results = [[]] + mockCheckWorkspaceAccess.mockResolvedValue(wsAdmin) + + const access = await getEditableSkillIds('ws', 'admin-user') + + expect(access.canAdminWorkspace).toBe(true) + }) + + it('grants nothing without workspace access, even with editor rows', async () => { + dbState.results = [[{ skillId: 's-mine' }]] + mockCheckWorkspaceAccess.mockResolvedValue(wsNone) + + const access = await getEditableSkillIds('ws', 'outsider') + + expect(access.canAdminWorkspace).toBe(false) + expect(access.editorSkillIds.size).toBe(0) + }) +}) + +describe('listSkillEditors', () => { + const roster = (users: Array<{ userId: string; permissionType: string }>) => + users.map((u) => ({ + userId: u.userId, + permissionType: u.permissionType, + name: `${u.userId}-name`, + email: `${u.userId}@x.com`, + image: null, + })) + + it('lists derived workspace admins plus explicit editors still in the roster', async () => { + dbState.results = [ + [ + { id: 'row-1', userId: 'writer' }, + { id: 'row-2', userId: 'ghost' }, + ], + ] + mockGetUsersWithPermissions.mockResolvedValue( + roster([ + { userId: 'boss', permissionType: 'admin' }, + { userId: 'writer', permissionType: 'write' }, + { userId: 'reader', permissionType: 'read' }, + ]) + ) + + const editors = await listSkillEditors({ id: 's1', workspaceId: 'ws' }) + const byUser = new Map(editors.map((e) => [e.userId, e])) + + expect(byUser.get('boss')).toMatchObject({ + id: 'workspace-admin-boss', + isWorkspaceAdmin: true, + userEmail: 'boss@x.com', + }) + expect(byUser.get('writer')).toMatchObject({ id: 'row-1', isWorkspaceAdmin: false }) + // Workspace members without a row are not editors. + expect(byUser.has('reader')).toBe(false) + // Rows for users no longer in the workspace never render. + expect(byUser.has('ghost')).toBe(false) + }) + + it('keeps a workspace admin flagged as derived even when they hold an explicit row', async () => { + dbState.results = [[{ id: 'row-1', userId: 'boss' }]] + mockGetUsersWithPermissions.mockResolvedValue( + roster([{ userId: 'boss', permissionType: 'admin' }]) + ) + + const editors = await listSkillEditors({ id: 's1', workspaceId: 'ws' }) + + expect(editors).toHaveLength(1) + expect(editors[0]).toMatchObject({ id: 'row-1', userId: 'boss', isWorkspaceAdmin: true }) + }) +}) + +describe('checkSkillsUpdateAccess', () => { + it('returns nothing for an empty id list without querying', async () => { + const result = await checkSkillsUpdateAccess({ workspaceId: 'ws', userId: 'u', skillIds: [] }) + expect(result.existingIds.size).toBe(0) + expect(result.denied).toEqual([]) + expect(dbMock.select).not.toHaveBeenCalled() + }) + + it('partitions resolvable ids and denies skills without an editor row', async () => { + dbState.results = [ + [ + { id: 's-mine', name: 'mine' }, + { id: 's-other', name: 'other' }, + ], + [{ skillId: 's-mine' }], + ] + mockCheckWorkspaceAccess.mockResolvedValue(wsWrite) + + const result = await checkSkillsUpdateAccess({ + workspaceId: 'ws', + userId: 'u', + skillIds: ['s-mine', 's-other', 's-create'], + }) + + expect(result.existingIds).toEqual(new Set(['s-mine', 's-other'])) + expect(result.denied).toEqual([{ id: 's-other', name: 'other' }]) + }) + + it('denies nothing for workspace admins', async () => { + dbState.results = [[{ id: 's-any', name: 'any' }], []] + mockCheckWorkspaceAccess.mockResolvedValue(wsAdmin) + + const result = await checkSkillsUpdateAccess({ + workspaceId: 'ws', + userId: 'admin-user', + skillIds: ['s-any'], + }) + + expect(result.denied).toEqual([]) + }) +}) + +describe('removeWorkspaceSkillMembershipsTx', () => { + it('returns 0 for an empty workspace list without querying', async () => { + const tx = { select: vi.fn(() => makeChain()), delete: vi.fn(() => makeChain()) } + expect(await removeWorkspaceSkillMembershipsTx(tx as never, [], 'u')).toBe(0) + expect(tx.delete).not.toHaveBeenCalled() + }) + + it('deletes every editor grant for the user in the workspaces and counts them', async () => { + const { eq } = await import('drizzle-orm') + dbState.results = [[{ id: 'm1' }, { id: 'm2' }]] + const tx = { select: vi.fn(() => makeChain()), delete: vi.fn(() => makeChain()) } + + expect(await removeWorkspaceSkillMembershipsTx(tx as never, ['ws'], 'u')).toBe(2) + expect(tx.delete).toHaveBeenCalledTimes(1) + // Rows are plain editor grants — the delete has no status filter, so a + // re-invited user starts with no edit rights until re-added. + expect(vi.mocked(eq).mock.calls).toContainEqual(['skillMember.userId', 'u']) + expect(vi.mocked(eq).mock.calls.some(([field]) => field === 'skillMember.status')).toBe(false) + }) +}) diff --git a/apps/sim/lib/skills/access.ts b/apps/sim/lib/skills/access.ts new file mode 100644 index 00000000000..ca835a47414 --- /dev/null +++ b/apps/sim/lib/skills/access.ts @@ -0,0 +1,212 @@ +import { db } from '@sim/db' +import { skill, skillMember } from '@sim/db/schema' +import { and, eq, inArray } from 'drizzle-orm' +import type { DbOrTx } from '@/lib/db/types' +import { + getUsersWithPermissions, + resolveWorkspaceAccess, + type WorkspaceAccess, +} from '@/lib/workspaces/permissions/utils' + +type SkillRecord = typeof skill.$inferSelect + +export interface SkillActorContext { + skill: SkillRecord | null + /** Whether the actor can see and use the skill — plain workspace access. */ + hasWorkspaceAccess: boolean + /** + * Whether the actor can edit, delete, and share the skill: an explicit + * `skill_member` editor row, or derived workspace admin (always, undemotable). + */ + canEdit: boolean +} + +/** + * Resolves the acting user's context for a single skill. Everyone with + * workspace access sees and uses every skill; editing is gated by the editors + * list. Builtin skills are code-only and have no editors; callers guard with + * `isBuiltinSkillId` before reaching this. + */ +export async function getSkillActorContext( + skillId: string, + userId: string +): Promise { + const [skillRow] = await db.select().from(skill).where(eq(skill.id, skillId)).limit(1) + + if (!skillRow?.workspaceId) { + return { skill: skillRow ?? null, hasWorkspaceAccess: false, canEdit: false } + } + + const [workspaceAccess, [editorRow]] = await Promise.all([ + resolveWorkspaceAccess(skillRow.workspaceId, userId), + db + .select({ id: skillMember.id }) + .from(skillMember) + .where(and(eq(skillMember.skillId, skillId), eq(skillMember.userId, userId))) + .limit(1), + ]) + + return { + skill: skillRow, + hasWorkspaceAccess: workspaceAccess.hasAccess, + canEdit: workspaceAccess.hasAccess && (workspaceAccess.canAdmin || !!editorRow), + } +} + +export interface EditableSkillIds { + /** Workspace admins are derived editors of every skill in the workspace. */ + canAdminWorkspace: boolean + /** Skills where the user holds an explicit editor row. */ + editorSkillIds: Set +} + +/** + * Batch edit-access surface for tagging many skills at once (list routes, + * upsert authorization): one workspace-access lookup plus one editor-row scan + * scoped to the workspace. A skill is editable when `canAdminWorkspace` or its + * id is in `editorSkillIds`. + * + * Pass `workspaceAccess` when the caller already resolved it to skip a + * redundant lookup. + */ +export async function getEditableSkillIds( + workspaceId: string, + userId: string, + options?: { workspaceAccess?: WorkspaceAccess } +): Promise { + const [workspaceAccess, editorRows] = await Promise.all([ + resolveWorkspaceAccess(workspaceId, userId, options?.workspaceAccess), + db + .select({ skillId: skillMember.skillId }) + .from(skillMember) + .innerJoin(skill, eq(skillMember.skillId, skill.id)) + .where(and(eq(skill.workspaceId, workspaceId), eq(skillMember.userId, userId))), + ]) + + if (!workspaceAccess.hasAccess) { + return { canAdminWorkspace: false, editorSkillIds: new Set() } + } + + return { + canAdminWorkspace: workspaceAccess.canAdmin, + editorSkillIds: new Set(editorRows.map((row) => row.skillId)), + } +} + +export interface SkillEditor { + /** Explicit row id, or a synthetic `workspace-admin-` id for derived admins without rows. */ + id: string + userId: string + userName: string | null + userEmail: string | null + userImage: string | null + /** Derived editors — always present, cannot be removed from the list. */ + isWorkspaceAdmin: boolean +} + +/** + * The editor roster for a skill: every workspace admin (derived, undemotable) + * plus every explicit-row user still in the workspace roster. Rows for users + * who left the workspace are ignored, exactly as edit enforcement ignores them. + */ +export async function listSkillEditors(skillRow: { + id: string + workspaceId: string +}): Promise { + const [explicitRows, workspaceMembers] = await Promise.all([ + db + .select({ id: skillMember.id, userId: skillMember.userId }) + .from(skillMember) + .where(eq(skillMember.skillId, skillRow.id)), + getUsersWithPermissions(skillRow.workspaceId), + ]) + + const rowByUser = new Map(explicitRows.map((row) => [row.userId, row])) + + const editors: SkillEditor[] = [] + for (const wsMember of workspaceMembers) { + const row = rowByUser.get(wsMember.userId) + const isWorkspaceAdmin = wsMember.permissionType === 'admin' + if (!row && !isWorkspaceAdmin) continue + + editors.push({ + id: row?.id ?? `workspace-admin-${wsMember.userId}`, + userId: wsMember.userId, + userName: wsMember.name, + userEmail: wsMember.email, + userImage: wsMember.image ?? null, + isWorkspaceAdmin, + }) + } + return editors +} + +export interface SkillsUpdateAccess { + /** Ids from the request that resolve to existing skills in the workspace. */ + existingIds: Set + /** Existing skills the user may not update (not an editor, not a workspace admin). */ + denied: Array<{ id: string; name: string }> +} + +/** + * Partitions an upsert request's skill ids for authorization: ids that resolve + * to existing workspace skills require skill editor access; unresolved ids are + * creates, gated by workspace write permission instead. + */ +export async function checkSkillsUpdateAccess(params: { + workspaceId: string + userId: string + skillIds: string[] + workspaceAccess?: WorkspaceAccess +}): Promise { + if (params.skillIds.length === 0) return { existingIds: new Set(), denied: [] } + + const rows = await db + .select({ id: skill.id, name: skill.name }) + .from(skill) + .where(and(eq(skill.workspaceId, params.workspaceId), inArray(skill.id, params.skillIds))) + + const existingIds = new Set(rows.map((row) => row.id)) + if (rows.length === 0) return { existingIds, denied: [] } + + const access = await getEditableSkillIds(params.workspaceId, params.userId, { + workspaceAccess: params.workspaceAccess, + }) + const denied = access.canAdminWorkspace + ? [] + : rows.filter((row) => !access.editorSkillIds.has(row.id)) + + return { existingIds, denied } +} + +/** + * Removes a user's skill editor grants across one or more workspaces when they + * leave (workspace removal, org removal/transfer). Rows are editor grants + * only — everyone in the workspace already sees and uses every skill — so a + * later re-invite lands them with no edit rights until re-added. Workspace + * admins are derived editors, so no promotion is needed to avoid orphaning a + * skill. Returns the number of grants removed. + */ +export async function removeWorkspaceSkillMembershipsTx( + tx: DbOrTx, + workspaceId: string | string[], + userId: string +): Promise { + const workspaceIds = Array.isArray(workspaceId) ? workspaceId : [workspaceId] + if (workspaceIds.length === 0) return 0 + + const removed = await tx + .delete(skillMember) + .where( + and( + eq(skillMember.userId, userId), + inArray( + skillMember.skillId, + tx.select({ id: skill.id }).from(skill).where(inArray(skill.workspaceId, workspaceIds)) + ) + ) + ) + .returning({ id: skillMember.id }) + + return removed.length +} diff --git a/apps/sim/lib/workflows/skills/operations.test.ts b/apps/sim/lib/workflows/skills/operations.test.ts index 9c5ebc167e1..62f49c01b57 100644 --- a/apps/sim/lib/workflows/skills/operations.test.ts +++ b/apps/sim/lib/workflows/skills/operations.test.ts @@ -4,10 +4,17 @@ import { dbChainMock, queueTableRows, resetDbChainMock, schemaMock } from '@sim/testing' import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' +const { getEditableSkillIdsMock } = vi.hoisted(() => ({ + getEditableSkillIdsMock: vi.fn(), +})) + vi.mock('@sim/db', () => ({ ...dbChainMock, ...schemaMock })) -vi.mock('@sim/utils/id', () => ({ generateShortId: () => 'gen-id' })) +vi.mock('@sim/utils/id', () => ({ generateId: () => 'gen-uuid', generateShortId: () => 'gen-id' })) +vi.mock('@/lib/skills/access', () => ({ + getEditableSkillIds: getEditableSkillIdsMock, +})) -import { listSkills } from '@/lib/workflows/skills/operations' +import { listSkills, listSkillsForUser } from '@/lib/workflows/skills/operations' describe('listSkills includeBuiltins', () => { beforeEach(() => { @@ -39,3 +46,76 @@ describe('listSkills includeBuiltins', () => { expect(result.some((s) => s.id.startsWith('builtin-'))).toBe(false) }) }) + +describe('listSkillsForUser', () => { + beforeEach(() => { + vi.clearAllMocks() + resetDbChainMock() + getEditableSkillIdsMock.mockResolvedValue({ + canAdminWorkspace: false, + editorSkillIds: new Set(), + }) + }) + + afterAll(() => { + resetDbChainMock() + }) + + it('returns every workspace skill tagged with edit access from editor rows', async () => { + queueTableRows(schemaMock.skill, [ + { id: 'sk-mine', name: 'mine', description: 'd', content: 'c', workspaceId: 'ws-1' }, + { id: 'sk-other', name: 'other', description: 'd', content: 'c', workspaceId: 'ws-1' }, + ]) + getEditableSkillIdsMock.mockResolvedValue({ + canAdminWorkspace: false, + editorSkillIds: new Set(['sk-mine']), + }) + + const result = await listSkillsForUser({ + workspaceId: 'ws-1', + userId: 'user-1', + includeBuiltins: false, + }) + + expect(result).toHaveLength(2) + expect(result.find((s) => s.id === 'sk-mine')).toMatchObject({ canEdit: true }) + expect(result.find((s) => s.id === 'sk-other')).toMatchObject({ canEdit: false }) + }) + + it('tags every skill editable for workspace admins', async () => { + queueTableRows(schemaMock.skill, [ + { id: 'sk-1', name: 'one', description: 'd', content: 'c', workspaceId: 'ws-1' }, + { id: 'sk-2', name: 'two', description: 'd', content: 'c', workspaceId: 'ws-1' }, + ]) + getEditableSkillIdsMock.mockResolvedValue({ + canAdminWorkspace: true, + editorSkillIds: new Set(), + }) + + const result = await listSkillsForUser({ + workspaceId: 'ws-1', + userId: 'admin-1', + includeBuiltins: false, + }) + + expect(result.every((s) => s.canEdit)).toBe(true) + }) + + it('always passes builtin skills through as non-editable', async () => { + const result = await listSkillsForUser({ workspaceId: 'ws-1', userId: 'user-1' }) + + expect(result.length).toBeGreaterThan(0) + expect(result.every((s) => s.id.startsWith('builtin-') && s.canEdit === false)).toBe(true) + }) + + it('lets a workspace skill sharing a builtin name override it for everyone', async () => { + queueTableRows(schemaMock.skill, [ + { id: 'sk-research', name: 'research', description: 'd', content: 'c', workspaceId: 'ws-1' }, + ]) + + const result = await listSkillsForUser({ workspaceId: 'ws-1', userId: 'user-1' }) + + expect(result.some((s) => s.id === 'sk-research')).toBe(true) + expect(result.some((s) => s.id === 'builtin-research')).toBe(false) + }) +}) diff --git a/apps/sim/lib/workflows/skills/operations.ts b/apps/sim/lib/workflows/skills/operations.ts index 7e0fcfb4c2f..4746cc0d5f7 100644 --- a/apps/sim/lib/workflows/skills/operations.ts +++ b/apps/sim/lib/workflows/skills/operations.ts @@ -1,15 +1,17 @@ import { db } from '@sim/db' -import { skill } from '@sim/db/schema' +import { skill, skillMember } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { generateShortId } from '@sim/utils/id' +import { generateId, generateShortId } from '@sim/utils/id' import { and, desc, eq, ne } from 'drizzle-orm' import { generateRequestId } from '@/lib/core/utils/request' +import { getEditableSkillIds } from '@/lib/skills/access' import { BUILTIN_SKILLS, type BuiltinSkill, getBuiltinSkillById, isBuiltinSkillId, } from '@/lib/workflows/skills/builtin-skills' +import type { WorkspaceAccess } from '@/lib/workspaces/permissions/utils' const logger = createLogger('SkillsOperations') @@ -57,6 +59,46 @@ export async function listSkills(params: { workspaceId: string; includeBuiltins? return [...builtins, ...dbRows] } +/** A skill row tagged with whether the caller can edit it (always false on builtins). */ +export type SkillWithAccess = typeof skill.$inferSelect & { canEdit: boolean } + +/** + * List every skill in the workspace, each tagged with whether the caller can + * edit it: workspace admins can edit all; others can edit skills where they + * hold an explicit editor row. Everyone in the workspace sees and uses every + * skill. Built-in template skills are code-only and never editable. + * + * Pass `workspaceAccess` when the caller already resolved it to skip a + * redundant lookup. + */ +export async function listSkillsForUser(params: { + workspaceId: string + userId: string + includeBuiltins?: boolean + workspaceAccess?: WorkspaceAccess +}): Promise { + const [dbRows, access] = await Promise.all([ + listSkills({ workspaceId: params.workspaceId, includeBuiltins: false }), + getEditableSkillIds(params.workspaceId, params.userId, { + workspaceAccess: params.workspaceAccess, + }), + ]) + + const tagged: SkillWithAccess[] = dbRows.map((row) => ({ + ...row, + canEdit: access.canAdminWorkspace || access.editorSkillIds.has(row.id), + })) + + if (params.includeBuiltins === false) return tagged + + // A workspace skill that shares a built-in's name overrides it for everyone. + const dbNames = new Set(tagged.map((r) => r.name.toLowerCase())) + const builtins: SkillWithAccess[] = BUILTIN_SKILLS.filter( + (b) => !dbNames.has(b.name.toLowerCase()) + ).map((b) => ({ ...builtinSkillRow(params.workspaceId, b), canEdit: false })) + return [...builtins, ...tagged] +} + /** * Fetch a single skill by id, scoped to a workspace. Built-in template skills * resolve from code; otherwise returns the DB row, or null when the skill does @@ -112,7 +154,11 @@ export interface TouchedSkill { } export interface UpsertSkillsResult { - /** Every skill in the workspace after the upsert, ordered by createdAt desc. */ + /** + * Every skill in the workspace after the upsert, ordered by createdAt desc. + * Empty when `returnSkills: false` — callers that re-fetch a filtered list + * themselves opt out so the transaction never re-reads full content bodies. + */ skills: Awaited> /** Only the skills this upsert created or updated, tagged with the operation. */ touched: TouchedSkill[] @@ -125,13 +171,14 @@ export interface UpsertSkillsResult { export async function upsertSkills(params: { skills: Array<{ id?: string - name: string - description: string - content: string + name?: string + description?: string + content?: string }> workspaceId: string userId: string requestId?: string + returnSkills?: boolean }): Promise { const { skills, workspaceId, userId, requestId = generateRequestId() } = params @@ -147,41 +194,53 @@ export async function upsertSkills(params: { const nowTime = new Date() if (s.id) { - const existingSkill = await tx + // Id-carrying items are updates and never fall through to a create: the + // caller's authorization partitioned on resolvability, so a vanished id + // must surface as not-found rather than an ungated (re-)create. + const [current] = await tx .select() .from(skill) .where(and(eq(skill.id, s.id), eq(skill.workspaceId, workspaceId))) .limit(1) - if (existingSkill.length > 0) { - if (s.name !== existingSkill[0].name) { - const nameConflict = await tx - .select({ id: skill.id }) - .from(skill) - .where( - and(eq(skill.workspaceId, workspaceId), eq(skill.name, s.name), ne(skill.id, s.id)) - ) - .limit(1) - - if (nameConflict.length > 0) { - throw new Error(`A skill with the name "${s.name}" already exists in this workspace`) - } - } + if (!current) { + throw new Error(`Skill not found: ${s.id}`) + } + + // Partial update: omitted fields keep their current values, so a + // sharing-only toggle can never clobber a concurrent content edit. + const nextName = s.name ?? current.name + if (nextName !== current.name) { + const nameConflict = await tx + .select({ id: skill.id }) + .from(skill) + .where( + and(eq(skill.workspaceId, workspaceId), eq(skill.name, nextName), ne(skill.id, s.id)) + ) + .limit(1) - await tx - .update(skill) - .set({ - name: s.name, - description: s.description, - content: s.content, - updatedAt: nowTime, - }) - .where(and(eq(skill.id, s.id), eq(skill.workspaceId, workspaceId))) - - touched.push({ id: s.id, name: s.name, operation: 'updated' }) - logger.info(`[${requestId}] Updated skill ${s.id}`) - continue + if (nameConflict.length > 0) { + throw new Error(`The skill name "${nextName}" is unavailable in this workspace`) + } } + + await tx + .update(skill) + .set({ + name: nextName, + description: s.description ?? current.description, + content: s.content ?? current.content, + updatedAt: nowTime, + }) + .where(and(eq(skill.id, s.id), eq(skill.workspaceId, workspaceId))) + + touched.push({ id: s.id, name: nextName, operation: 'updated' }) + logger.info(`[${requestId}] Updated skill ${s.id}`) + continue + } + + if (!s.name || !s.description || !s.content) { + throw new Error('Skill name, description, and content are required to create a skill') } const duplicateName = await tx @@ -191,7 +250,7 @@ export async function upsertSkills(params: { .limit(1) if (duplicateName.length > 0) { - throw new Error(`A skill with the name "${s.name}" already exists in this workspace`) + throw new Error(`The skill name "${s.name}" is unavailable in this workspace`) } const newId = generateShortId() @@ -206,10 +265,25 @@ export async function upsertSkills(params: { updatedAt: nowTime, }) + // The creator becomes an editor; workspace admins are derived editors + // with no rows, and everyone in the workspace can already use the skill. + await tx.insert(skillMember).values({ + id: generateId(), + skillId: newId, + userId, + invitedBy: userId, + createdAt: nowTime, + updatedAt: nowTime, + }) + touched.push({ id: newId, name: s.name, operation: 'created' }) logger.info(`[${requestId}] Created skill "${s.name}"`) } + if (params.returnSkills === false) { + return { skills: [], touched } + } + const resultSkills = await tx .select() .from(skill) diff --git a/apps/sim/lib/workspaces/permissions/utils.ts b/apps/sim/lib/workspaces/permissions/utils.ts index 81c12739b40..6afc4f66554 100644 --- a/apps/sim/lib/workspaces/permissions/utils.ts +++ b/apps/sim/lib/workspaces/permissions/utils.ts @@ -164,6 +164,21 @@ export async function checkWorkspaceAccess( return { exists: true, hasAccess, canWrite, canAdmin, workspace: ws, permission } } +/** + * Returns `provided` when it was resolved for this exact workspace, otherwise + * resolves fresh. The id match is what keeps a caller from authorizing against + * another workspace's cached access - every access-reuse path must go through + * this rather than hand-rolling the comparison. + */ +export async function resolveWorkspaceAccess( + workspaceId: string, + userId: string, + provided?: WorkspaceAccess +): Promise { + if (provided && provided.workspace?.id === workspaceId) return provided + return checkWorkspaceAccess(workspaceId, userId) +} + /** * Thrown when a user attempts to access a workspace they don't have access to, * or that doesn't exist / has been archived. Carries `statusCode = 403` so the diff --git a/apps/sim/app/workspace/[workspaceId]/components/credential-detail/sharing.test.ts b/apps/sim/lib/workspaces/sharing.test.ts similarity index 96% rename from apps/sim/app/workspace/[workspaceId]/components/credential-detail/sharing.test.ts rename to apps/sim/lib/workspaces/sharing.test.ts index c762ac9f0fc..1c202684093 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/credential-detail/sharing.test.ts +++ b/apps/sim/lib/workspaces/sharing.test.ts @@ -2,7 +2,7 @@ * @vitest-environment node */ import { describe, expect, it } from 'vitest' -import { partitionSettledFailures, resolveAddEmail } from './sharing' +import { partitionSettledFailures, resolveAddEmail } from '@/lib/workspaces/sharing' describe('resolveAddEmail', () => { const ctx = { diff --git a/apps/sim/app/workspace/[workspaceId]/components/credential-detail/sharing.ts b/apps/sim/lib/workspaces/sharing.ts similarity index 88% rename from apps/sim/app/workspace/[workspaceId]/components/credential-detail/sharing.ts rename to apps/sim/lib/workspaces/sharing.ts index f315ba0eedf..a42eed3bd21 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/credential-detail/sharing.ts +++ b/apps/sim/lib/workspaces/sharing.ts @@ -1,14 +1,15 @@ export interface ResolveAddEmailContext { /** Lowercased email -> workspace userId for every workspace member. */ workspaceUserIdByEmail: Map - /** Lowercased emails that already have access to the credential. */ + /** Lowercased emails that already have access to the resource. */ existingMemberEmails: Set } export type ResolveAddEmailResult = { userId: string } | { error: string } /** - * Decide whether a format-valid email can be added to a credential: it must + * Decide whether a format-valid email can be added to a shared resource + * (credential, skill): it must * belong to a workspace member and not already have access. Matching is * case-insensitive (the context map/set are keyed by lowercased email) while * error messages echo the email as the user typed it. Returns the resolved diff --git a/bun.lock b/bun.lock index 69f9acb71ac..849269e0794 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "simstudio", @@ -601,6 +602,7 @@ "postgres": "^3.4.5", "react": "19.2.4", "react-dom": "19.2.4", + "zod": "4.3.6", }, "packages": { "@1password/sdk": ["@1password/sdk@0.3.1", "", { "dependencies": { "@1password/sdk-core": "0.3.1" } }, "sha512-20zbQfqsjcECT0gvnAw4zONJDt3XQgNH946pZR0NV1Qxukyaz/DKB0cBnBNCCEWZg93Bah8poaR6gJCyuNX14w=="], @@ -3095,7 +3097,7 @@ "lru.min": ["lru.min@1.1.4", "", {}, "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA=="], - "lucide-react": ["lucide-react@1.23.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-38BpJcD0JhFosxHApP/BYsBetLpQFRoTRzEzstM/XCc3jsAG7wqaY1lgVwxiUe3xqYE+lNxo2PkCmYwXWrwwIw=="], + "lucide-react": ["lucide-react@0.479.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-aBhNnveRhorBOK7uA4gDjgaf+YlHMdMhQ/3cupk6exM10hWlEU+2QtWYOfhXhjAsmdb6LeKR+NZnow4UxRRiTQ=="], "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], @@ -4211,16 +4213,10 @@ "@better-auth/core/jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="], - "@better-auth/core/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], - "@better-auth/sso/jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="], "@better-auth/sso/tldts": ["tldts@6.1.86", "", { "dependencies": { "tldts-core": "^6.1.86" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ=="], - "@better-auth/sso/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], - - "@better-auth/stripe/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], - "@browserbasehq/sdk/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], "@browserbasehq/sdk/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], @@ -4535,10 +4531,6 @@ "@sim/db/@types/node": ["@types/node@22.19.21", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-VMeFBSCKQKmm2swI2kW51SFusDqekC6q9trBCvJ/JliDchFSuoYYKN7yVNjPthP1HKZcx3U1gI/wTcEBjEFKTA=="], - "@sim/emcn/lucide-react": ["lucide-react@0.479.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-aBhNnveRhorBOK7uA4gDjgaf+YlHMdMhQ/3cupk6exM10hWlEU+2QtWYOfhXhjAsmdb6LeKR+NZnow4UxRRiTQ=="], - - "@sim/workflow-renderer/lucide-react": ["lucide-react@0.479.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-aBhNnveRhorBOK7uA4gDjgaf+YlHMdMhQ/3cupk6exM10hWlEU+2QtWYOfhXhjAsmdb6LeKR+NZnow4UxRRiTQ=="], - "@smithy/middleware-compression/fflate": ["fflate@0.8.1", "", {}, "sha512-/exOvEuc+/iaUm105QIiOt4LpBdMTWsXxqR0HDF35vx3fmaKzw7354gTilCh5rkzEt8WYyG//ku3h3nRmd7CHQ=="], "@socket.io/redis-adapter/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], @@ -4595,8 +4587,6 @@ "@trigger.dev/core/tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], - "@trigger.dev/core/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "@trigger.dev/sdk/@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], "@trigger.dev/sdk/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.36.0", "", {}, "sha512-TtxJSRD8Ohxp6bKkhrm27JRHAxPczQA7idtcTOMYI+wQRRrfgqxHv1cFbCApcSnNjtXkmzFozn6jQtFrOmbjPQ=="], @@ -4635,8 +4625,6 @@ "better-auth/jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="], - "better-auth/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], - "bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], "bl/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], @@ -4733,18 +4721,20 @@ "fumadocs-mdx/js-yaml": ["js-yaml@4.2.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw=="], - "fumadocs-mdx/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], - "fumadocs-openapi/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], "fumadocs-openapi/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], "fumadocs-openapi/js-yaml": ["js-yaml@4.2.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw=="], + "fumadocs-openapi/lucide-react": ["lucide-react@1.23.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-38BpJcD0JhFosxHApP/BYsBetLpQFRoTRzEzstM/XCc3jsAG7wqaY1lgVwxiUe3xqYE+lNxo2PkCmYwXWrwwIw=="], + "fumadocs-openapi/tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="], "fumadocs-ui/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], + "fumadocs-ui/lucide-react": ["lucide-react@1.23.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-38BpJcD0JhFosxHApP/BYsBetLpQFRoTRzEzstM/XCc3jsAG7wqaY1lgVwxiUe3xqYE+lNxo2PkCmYwXWrwwIw=="], + "fumadocs-ui/tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="], "gaxios/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], @@ -4903,8 +4893,6 @@ "sharp/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], - "sim/lucide-react": ["lucide-react@0.479.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-aBhNnveRhorBOK7uA4gDjgaf+YlHMdMhQ/3cupk6exM10hWlEU+2QtWYOfhXhjAsmdb6LeKR+NZnow4UxRRiTQ=="], - "sim/tailwindcss": ["tailwindcss@3.4.19", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.21.7", "lilconfig": "^3.1.3", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", "postcss-nested": "^6.2.0", "postcss-selector-parser": "^6.1.2", "resolve": "^1.22.8", "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ=="], "simstudio/@types/node": ["@types/node@20.19.43", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA=="], @@ -4969,10 +4957,6 @@ "xml2js/xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="], - "zod-error/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - - "zod-validation-error/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "zrender/tslib": ["tslib@2.3.0", "", {}, "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="], "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], diff --git a/package.json b/package.json index 374ffb1f23c..95b4486ff50 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,8 @@ "drizzle-orm": "^0.45.2", "postgres": "^3.4.5", "minimatch": "^10.2.5", - "mermaid": "11.15.0" + "mermaid": "11.15.0", + "zod": "4.3.6" }, "devDependencies": { "@biomejs/biome": "2.0.0-beta.5", diff --git a/packages/audit/src/types.ts b/packages/audit/src/types.ts index 90e02afe2af..a7bfb980408 100644 --- a/packages/audit/src/types.ts +++ b/packages/audit/src/types.ts @@ -159,6 +159,8 @@ export const AuditAction = { SKILL_CREATED: 'skill.created', SKILL_UPDATED: 'skill.updated', SKILL_DELETED: 'skill.deleted', + SKILL_MEMBER_ADDED: 'skill_member.added', + SKILL_MEMBER_REMOVED: 'skill_member.removed', // Schedules SCHEDULE_CREATED: 'schedule.created', diff --git a/packages/db/migrations/0267_dark_romulus.sql b/packages/db/migrations/0267_dark_romulus.sql new file mode 100644 index 00000000000..b75860a6ee7 --- /dev/null +++ b/packages/db/migrations/0267_dark_romulus.sql @@ -0,0 +1,29 @@ +CREATE TABLE "skill_member" ( + "id" text PRIMARY KEY NOT NULL, + "skill_id" text NOT NULL, + "user_id" text NOT NULL, + "invited_by" text, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "skill_member" ADD CONSTRAINT "skill_member_skill_id_skill_id_fk" FOREIGN KEY ("skill_id") REFERENCES "public"."skill"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "skill_member" ADD CONSTRAINT "skill_member_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "skill_member" ADD CONSTRAINT "skill_member_invited_by_user_id_fk" FOREIGN KEY ("invited_by") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "skill_member_user_id_idx" ON "skill_member" USING btree ("user_id");--> statement-breakpoint +CREATE UNIQUE INDEX "skill_member_unique" ON "skill_member" USING btree ("skill_id","user_id");--> statement-breakpoint +-- Backfill: existing skills predate the editors list, where any workspace `write` user +-- could edit any skill. Grant those users an explicit editor row so their edit rights +-- survive the cutover. Workspace admins get no rows (they are derived editors at +-- runtime), which the write-only permission join guarantees — the permissions table is +-- unique on (user_id, entity_type, entity_id). Everyone with workspace access can see +-- and use every skill regardless of rows, so no other grants are needed. +-- Idempotent and deploy-window re-runnable: deterministic ids + ON CONFLICT DO NOTHING, +-- so re-running this INSERT once after full cutover heals any skill created by a +-- still-running old pod during the deploy window. +INSERT INTO "skill_member" ("id", "skill_id", "user_id", "invited_by", "created_at", "updated_at") +SELECT 'skillm_' || md5(s."id" || ':' || p."user_id"), s."id", p."user_id", s."user_id", now(), now() +FROM "skill" s +INNER JOIN "permissions" p ON p."entity_type" = 'workspace' AND p."entity_id" = s."workspace_id" AND p."permission_type" = 'write' +WHERE s."workspace_id" IS NOT NULL +ON CONFLICT DO NOTHING; \ No newline at end of file diff --git a/packages/db/migrations/meta/0267_snapshot.json b/packages/db/migrations/meta/0267_snapshot.json new file mode 100644 index 00000000000..4be63a659fd --- /dev/null +++ b/packages/db/migrations/meta/0267_snapshot.json @@ -0,0 +1,17529 @@ +{ + "id": "25435972-1fe1-4766-a728-d8e5c8818168", + "prevId": "dd2e88e1-ce36-426e-9857-a9f8ab6141fb", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.academy_certificate": { + "name": "academy_certificate", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "course_id": { + "name": "course_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "academy_cert_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "issued_at": { + "name": "issued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "certificate_number": { + "name": "certificate_number", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "academy_certificate_user_id_idx": { + "name": "academy_certificate_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_course_id_idx": { + "name": "academy_certificate_course_id_idx", + "columns": [ + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_user_course_unique": { + "name": "academy_certificate_user_course_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_number_idx": { + "name": "academy_certificate_number_idx", + "columns": [ + { + "expression": "certificate_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_status_idx": { + "name": "academy_certificate_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "academy_certificate_user_id_user_id_fk": { + "name": "academy_certificate_user_id_user_id_fk", + "tableFrom": "academy_certificate", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "academy_certificate_certificate_number_unique": { + "name": "academy_certificate_certificate_number_unique", + "nullsNotDistinct": false, + "columns": ["certificate_number"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_user_id_idx": { + "name": "account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_account_on_account_id_provider_id": { + "name": "idx_account_on_account_id_provider_id", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_key": { + "name": "api_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'personal'" + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "api_key_workspace_type_idx": { + "name": "api_key_workspace_type_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_user_type_idx": { + "name": "api_key_user_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_key_hash_idx": { + "name": "api_key_key_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_key_user_id_user_id_fk": { + "name": "api_key_user_id_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_workspace_id_workspace_id_fk": { + "name": "api_key_workspace_id_workspace_id_fk", + "tableFrom": "api_key", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_created_by_user_id_fk": { + "name": "api_key_created_by_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_key_key_unique": { + "name": "api_key_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": { + "workspace_type_check": { + "name": "workspace_type_check", + "value": "(type = 'workspace' AND workspace_id IS NOT NULL) OR (type = 'personal' AND workspace_id IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.async_jobs": { + "name": "async_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "run_at": { + "name": "run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "async_jobs_status_started_at_idx": { + "name": "async_jobs_status_started_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_status_completed_at_idx": { + "name": "async_jobs_status_completed_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "completed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_pending_run_at_idx": { + "name": "async_jobs_schedule_pending_run_at_idx", + "columns": [ + { + "expression": "run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_processing_started_at_idx": { + "name": "async_jobs_schedule_processing_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'processing'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_name": { + "name": "actor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_log_workspace_created_idx": { + "name": "audit_log_workspace_created_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_workspace_created_at_id_idx": { + "name": "audit_log_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_actor_created_idx": { + "name": "audit_log_actor_created_idx", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_resource_idx": { + "name": "audit_log_resource_idx", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_action_idx": { + "name": "audit_log_action_idx", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_log_workspace_id_workspace_id_fk": { + "name": "audit_log_workspace_id_workspace_id_fk", + "tableFrom": "audit_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_log_actor_id_user_id_fk": { + "name": "audit_log_actor_id_user_id_fk", + "tableFrom": "audit_log", + "tableTo": "user", + "columnsFrom": ["actor_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.background_work_status": { + "name": "background_work_status", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "background_work_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "background_work_status_value", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "background_work_status_workspace_status_idx": { + "name": "background_work_status_workspace_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "background_work_status_workflow_status_idx": { + "name": "background_work_status_workflow_status_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "background_work_status_meta_child_ws_idx": { + "name": "background_work_status_meta_child_ws_idx", + "columns": [ + { + "expression": "(\"metadata\" ->> 'childWorkspaceId')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "background_work_status_meta_other_ws_idx": { + "name": "background_work_status_meta_other_ws_idx", + "columns": [ + { + "expression": "(\"metadata\" ->> 'otherWorkspaceId')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "background_work_status_workspace_id_workspace_id_fk": { + "name": "background_work_status_workspace_id_workspace_id_fk", + "tableFrom": "background_work_status", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "background_work_status_workflow_id_workflow_id_fk": { + "name": "background_work_status_workflow_id_workflow_id_fk", + "tableFrom": "background_work_status", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat": { + "name": "chat", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "output_configs": { + "name": "output_configs", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "include_thinking": { + "name": "include_thinking", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "identifier_idx": { + "name": "identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"chat\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_archived_at_partial_idx": { + "name": "chat_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"chat\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_on_workflow_id_archived_at": { + "name": "idx_chat_on_workflow_id_archived_at", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_workflow_id_workflow_id_fk": { + "name": "chat_workflow_id_workflow_id_fk", + "tableFrom": "chat", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_user_id_user_id_fk": { + "name": "chat_user_id_user_id_fk", + "tableFrom": "chat", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_async_tool_calls": { + "name": "copilot_async_tool_calls", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "args": { + "name": "args", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "status": { + "name": "status", + "type": "copilot_async_tool_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "claimed_by": { + "name": "claimed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_async_tool_calls_run_id_idx": { + "name": "copilot_async_tool_calls_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_checkpoint_id_idx": { + "name": "copilot_async_tool_calls_checkpoint_id_idx", + "columns": [ + { + "expression": "checkpoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_tool_call_id_idx": { + "name": "copilot_async_tool_calls_tool_call_id_idx", + "columns": [ + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_status_idx": { + "name": "copilot_async_tool_calls_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_run_status_idx": { + "name": "copilot_async_tool_calls_run_status_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_tool_call_id_unique": { + "name": "copilot_async_tool_calls_tool_call_id_unique", + "columns": [ + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_async_tool_calls_run_id_copilot_runs_id_fk": { + "name": "copilot_async_tool_calls_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk": { + "name": "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_run_checkpoints", + "columnsFrom": ["checkpoint_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_chats": { + "name": "copilot_chats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "chat_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'copilot'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'claude-3-7-sonnet-latest'" + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preview_yaml": { + "name": "preview_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plan_artifact": { + "name": "plan_artifact", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "resources": { + "name": "resources", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "pinned": { + "name": "pinned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_chats_user_id_idx": { + "name": "copilot_chats_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workflow_id_idx": { + "name": "copilot_chats_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workflow_idx": { + "name": "copilot_chats_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workspace_idx": { + "name": "copilot_chats_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_created_at_idx": { + "name": "copilot_chats_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_updated_at_idx": { + "name": "copilot_chats_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workspace_created_at_id_idx": { + "name": "copilot_chats_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workspace_deleted_partial_idx": { + "name": "copilot_chats_user_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_chats\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_chats_user_id_user_id_fk": { + "name": "copilot_chats_user_id_user_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workflow_id_workflow_id_fk": { + "name": "copilot_chats_workflow_id_workflow_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workspace_id_workspace_id_fk": { + "name": "copilot_chats_workspace_id_workspace_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_feedback": { + "name": "copilot_feedback", + "schema": "", + "columns": { + "feedback_id": { + "name": "feedback_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_query": { + "name": "user_query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_response": { + "name": "agent_response", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_positive": { + "name": "is_positive", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_yaml": { + "name": "workflow_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_feedback_user_id_idx": { + "name": "copilot_feedback_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_chat_id_idx": { + "name": "copilot_feedback_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_user_chat_idx": { + "name": "copilot_feedback_user_chat_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_is_positive_idx": { + "name": "copilot_feedback_is_positive_idx", + "columns": [ + { + "expression": "is_positive", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_created_at_idx": { + "name": "copilot_feedback_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_feedback_user_id_user_id_fk": { + "name": "copilot_feedback_user_id_user_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_feedback_chat_id_copilot_chats_id_fk": { + "name": "copilot_feedback_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_messages": { + "name": "copilot_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_message_id": { + "name": "parent_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokens_in": { + "name": "tokens_in", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tokens_out": { + "name": "tokens_out", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_messages_chat_message_unique": { + "name": "copilot_messages_chat_message_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_created_at_idx": { + "name": "copilot_messages_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_seq_idx": { + "name": "copilot_messages_chat_seq_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_stream_idx": { + "name": "copilot_messages_chat_stream_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"stream_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_user_created_at_idx": { + "name": "copilot_messages_user_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"role\" = 'user' AND \"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_messages_chat_id_copilot_chats_id_fk": { + "name": "copilot_messages_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_messages", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_run_checkpoints": { + "name": "copilot_run_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pending_tool_call_id": { + "name": "pending_tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conversation_snapshot": { + "name": "conversation_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "agent_state": { + "name": "agent_state", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "provider_request": { + "name": "provider_request", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_run_checkpoints_run_id_idx": { + "name": "copilot_run_checkpoints_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_pending_tool_call_id_idx": { + "name": "copilot_run_checkpoints_pending_tool_call_id_idx", + "columns": [ + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_run_pending_tool_unique": { + "name": "copilot_run_checkpoints_run_pending_tool_unique", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_run_checkpoints_run_id_copilot_runs_id_fk": { + "name": "copilot_run_checkpoints_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_run_checkpoints", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_runs": { + "name": "copilot_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent": { + "name": "agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "copilot_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "request_context": { + "name": "request_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "copilot_runs_execution_id_idx": { + "name": "copilot_runs_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_parent_run_id_idx": { + "name": "copilot_runs_parent_run_id_idx", + "columns": [ + { + "expression": "parent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_id_idx": { + "name": "copilot_runs_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_user_id_idx": { + "name": "copilot_runs_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workflow_id_idx": { + "name": "copilot_runs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_id_idx": { + "name": "copilot_runs_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_status_idx": { + "name": "copilot_runs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_execution_idx": { + "name": "copilot_runs_chat_execution_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_execution_started_at_idx": { + "name": "copilot_runs_execution_started_at_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_completed_at_id_idx": { + "name": "copilot_runs_workspace_completed_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"completed_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_stream_id_unique": { + "name": "copilot_runs_stream_id_unique", + "columns": [ + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_runs_chat_id_copilot_chats_id_fk": { + "name": "copilot_runs_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_user_id_user_id_fk": { + "name": "copilot_runs_user_id_user_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workflow_id_workflow_id_fk": { + "name": "copilot_runs_workflow_id_workflow_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workspace_id_workspace_id_fk": { + "name": "copilot_runs_workspace_id_workspace_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_workflow_read_hashes": { + "name": "copilot_workflow_read_hashes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_workflow_read_hashes_chat_id_idx": { + "name": "copilot_workflow_read_hashes_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_workflow_id_idx": { + "name": "copilot_workflow_read_hashes_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_chat_workflow_unique": { + "name": "copilot_workflow_read_hashes_chat_workflow_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk": { + "name": "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_workflow_read_hashes_workflow_id_workflow_id_fk": { + "name": "copilot_workflow_read_hashes_workflow_id_workflow_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential": { + "name": "credential", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "credential_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_key": { + "name": "env_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_owner_user_id": { + "name": "env_owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_service_account_key": { + "name": "encrypted_service_account_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_workspace_id_idx": { + "name": "credential_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_type_idx": { + "name": "credential_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_provider_id_idx": { + "name": "credential_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_account_id_idx": { + "name": "credential_account_id_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_env_owner_user_id_idx": { + "name": "credential_env_owner_user_id_idx", + "columns": [ + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_account_unique": { + "name": "credential_workspace_account_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "account_id IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_env_unique": { + "name": "credential_workspace_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_workspace'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_personal_env_unique": { + "name": "credential_workspace_personal_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_personal'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_workspace_id_workspace_id_fk": { + "name": "credential_workspace_id_workspace_id_fk", + "tableFrom": "credential", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_account_id_account_id_fk": { + "name": "credential_account_id_account_id_fk", + "tableFrom": "credential", + "tableTo": "account", + "columnsFrom": ["account_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_env_owner_user_id_user_id_fk": { + "name": "credential_env_owner_user_id_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["env_owner_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_created_by_user_id_fk": { + "name": "credential_created_by_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credential_oauth_source_check": { + "name": "credential_oauth_source_check", + "value": "(type <> 'oauth') OR (account_id IS NOT NULL AND provider_id IS NOT NULL)" + }, + "credential_workspace_env_source_check": { + "name": "credential_workspace_env_source_check", + "value": "(type <> 'env_workspace') OR (env_key IS NOT NULL AND env_owner_user_id IS NULL)" + }, + "credential_personal_env_source_check": { + "name": "credential_personal_env_source_check", + "value": "(type <> 'env_personal') OR (env_key IS NOT NULL AND env_owner_user_id IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.credential_member": { + "name": "credential_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "credential_member_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "credential_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_member_user_id_idx": { + "name": "credential_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_role_idx": { + "name": "credential_member_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_status_idx": { + "name": "credential_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_unique": { + "name": "credential_member_unique", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_member_credential_id_credential_id_fk": { + "name": "credential_member_credential_id_credential_id_fk", + "tableFrom": "credential_member", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_user_id_user_id_fk": { + "name": "credential_member_user_id_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_invited_by_user_id_fk": { + "name": "credential_member_invited_by_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_block": { + "name": "custom_block", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inputs": { + "name": "inputs", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "outputs": { + "name": "outputs", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_block_organization_id_idx": { + "name": "custom_block_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_block_workflow_id_idx": { + "name": "custom_block_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_block_organization_type_unique": { + "name": "custom_block_organization_type_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_block_organization_id_organization_id_fk": { + "name": "custom_block_organization_id_organization_id_fk", + "tableFrom": "custom_block", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_block_workflow_id_workflow_id_fk": { + "name": "custom_block_workflow_id_workflow_id_fk", + "tableFrom": "custom_block", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_block_created_by_user_id_fk": { + "name": "custom_block_created_by_user_id_fk", + "tableFrom": "custom_block", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_tools": { + "name": "custom_tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_tools_workspace_id_idx": { + "name": "custom_tools_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_tools_workspace_title_unique": { + "name": "custom_tools_workspace_title_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_tools_workspace_id_workspace_id_fk": { + "name": "custom_tools_workspace_id_workspace_id_fk", + "tableFrom": "custom_tools", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_tools_user_id_user_id_fk": { + "name": "custom_tools_user_id_user_id_fk", + "tableFrom": "custom_tools", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drain_runs": { + "name": "data_drain_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "drain_id": { + "name": "drain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "data_drain_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "data_drain_run_trigger", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "rows_exported": { + "name": "rows_exported", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "bytes_written": { + "name": "bytes_written", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cursor_before": { + "name": "cursor_before", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cursor_after": { + "name": "cursor_after", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locators": { + "name": "locators", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + } + }, + "indexes": { + "data_drain_runs_drain_started_idx": { + "name": "data_drain_runs_drain_started_idx", + "columns": [ + { + "expression": "drain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drain_runs_drain_id_data_drains_id_fk": { + "name": "data_drain_runs_drain_id_data_drains_id_fk", + "tableFrom": "data_drain_runs", + "tableTo": "data_drains", + "columnsFrom": ["drain_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drains": { + "name": "data_drains", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "data_drain_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_type": { + "name": "destination_type", + "type": "data_drain_destination", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_config": { + "name": "destination_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "destination_credentials": { + "name": "destination_credentials", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule_cadence": { + "name": "schedule_cadence", + "type": "data_drain_cadence", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "cursor": { + "name": "cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_success_at": { + "name": "last_success_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "data_drains_org_idx": { + "name": "data_drains_org_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_due_idx": { + "name": "data_drains_due_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_org_name_unique": { + "name": "data_drains_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drains_organization_id_organization_id_fk": { + "name": "data_drains_organization_id_organization_id_fk", + "tableFrom": "data_drains", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "data_drains_created_by_user_id_fk": { + "name": "data_drains_created_by_user_id_fk", + "tableFrom": "data_drains", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.docs_embeddings": { + "name": "docs_embeddings", + "schema": "", + "columns": { + "chunk_id": { + "name": "chunk_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chunk_text": { + "name": "chunk_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_document": { + "name": "source_document", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_link": { + "name": "source_link", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_text": { + "name": "header_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_level": { + "name": "header_level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "chunk_text_tsv": { + "name": "chunk_text_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"docs_embeddings\".\"chunk_text\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "docs_emb_source_document_idx": { + "name": "docs_emb_source_document_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_header_level_idx": { + "name": "docs_emb_header_level_idx", + "columns": [ + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_source_header_idx": { + "name": "docs_emb_source_header_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_model_idx": { + "name": "docs_emb_model_idx", + "columns": [ + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_created_at_idx": { + "name": "docs_emb_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_embedding_vector_hnsw_idx": { + "name": "docs_embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "docs_emb_metadata_gin_idx": { + "name": "docs_emb_metadata_gin_idx", + "columns": [ + { + "expression": "metadata", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "docs_emb_chunk_text_fts_idx": { + "name": "docs_emb_chunk_text_fts_idx", + "columns": [ + { + "expression": "chunk_text_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "docs_embedding_not_null_check": { + "name": "docs_embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + }, + "docs_header_level_check": { + "name": "docs_header_level_check", + "value": "\"header_level\" >= 1 AND \"header_level\" <= 6" + } + }, + "isRLSEnabled": false + }, + "public.document": { + "name": "document", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_url": { + "name": "file_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "character_count": { + "name": "character_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_status": { + "name": "processing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_completed_at": { + "name": "processing_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_error": { + "name": "processing_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "user_excluded": { + "name": "user_excluded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "doc_kb_id_idx": { + "name": "doc_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_filename_idx": { + "name": "doc_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_processing_status_idx": { + "name": "doc_processing_status_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "processing_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_external_id_idx": { + "name": "doc_connector_external_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_id_idx": { + "name": "doc_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_storage_key_idx": { + "name": "doc_storage_key_idx", + "columns": [ + { + "expression": "storage_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"storage_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_archived_at_partial_idx": { + "name": "doc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_deleted_at_partial_idx": { + "name": "doc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag1_idx": { + "name": "doc_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag2_idx": { + "name": "doc_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag3_idx": { + "name": "doc_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag4_idx": { + "name": "doc_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag5_idx": { + "name": "doc_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag6_idx": { + "name": "doc_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag7_idx": { + "name": "doc_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number1_idx": { + "name": "doc_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number2_idx": { + "name": "doc_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number3_idx": { + "name": "doc_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number4_idx": { + "name": "doc_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number5_idx": { + "name": "doc_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date1_idx": { + "name": "doc_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date2_idx": { + "name": "doc_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean1_idx": { + "name": "doc_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean2_idx": { + "name": "doc_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean3_idx": { + "name": "doc_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_knowledge_base_id_knowledge_base_id_fk": { + "name": "document_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_connector_id_knowledge_connector_id_fk": { + "name": "document_connector_id_knowledge_connector_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "document_uploaded_by_user_id_fk": { + "name": "document_uploaded_by_user_id_fk", + "tableFrom": "document", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.embedding": { + "name": "embedding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_index": { + "name": "chunk_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chunk_hash": { + "name": "chunk_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_length": { + "name": "content_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "start_offset": { + "name": "start_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_offset": { + "name": "end_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "content_tsv": { + "name": "content_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"embedding\".\"content\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "emb_kb_id_idx": { + "name": "emb_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_id_idx": { + "name": "emb_doc_id_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_chunk_idx": { + "name": "emb_doc_chunk_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chunk_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_model_idx": { + "name": "emb_kb_model_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_enabled_idx": { + "name": "emb_kb_enabled_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_enabled_idx": { + "name": "emb_doc_enabled_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "embedding_vector_hnsw_idx": { + "name": "embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "emb_tag1_idx": { + "name": "emb_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag2_idx": { + "name": "emb_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag3_idx": { + "name": "emb_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag4_idx": { + "name": "emb_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag5_idx": { + "name": "emb_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag6_idx": { + "name": "emb_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag7_idx": { + "name": "emb_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number1_idx": { + "name": "emb_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number2_idx": { + "name": "emb_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number3_idx": { + "name": "emb_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number4_idx": { + "name": "emb_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number5_idx": { + "name": "emb_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date1_idx": { + "name": "emb_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date2_idx": { + "name": "emb_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean1_idx": { + "name": "emb_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean2_idx": { + "name": "emb_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean3_idx": { + "name": "emb_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_content_fts_idx": { + "name": "emb_content_fts_idx", + "columns": [ + { + "expression": "content_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "embedding_knowledge_base_id_knowledge_base_id_fk": { + "name": "embedding_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "embedding", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "embedding_document_id_document_id_fk": { + "name": "embedding_document_id_document_id_fk", + "tableFrom": "embedding", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_not_null_check": { + "name": "embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_user_id_user_id_fk": { + "name": "environment_user_id_user_id_fk", + "tableFrom": "environment", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_user_id_unique": { + "name": "environment_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_value_dependencies": { + "name": "execution_large_value_dependencies", + "schema": "", + "columns": { + "parent_key": { + "name": "parent_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_key": { + "name": "child_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_large_value_dependencies_workspace_parent_key_idx": { + "name": "execution_large_value_dependencies_workspace_parent_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_value_dependencies_workspace_child_key_idx": { + "name": "execution_large_value_dependencies_workspace_child_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_value_dependencies_workspace_id_workspace_id_fk": { + "name": "execution_large_value_dependencies_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_value_dependencies", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "execution_large_value_dependencies_parent_key_child_key_pk": { + "name": "execution_large_value_dependencies_parent_key_child_key_pk", + "columns": ["parent_key", "child_key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_value_references": { + "name": "execution_large_value_references", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "execution_large_value_reference_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_large_value_references_workspace_execution_source_idx": { + "name": "execution_large_value_references_workspace_execution_source_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_value_references_workspace_id_workspace_id_fk": { + "name": "execution_large_value_references_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_value_references", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_large_value_references_workflow_id_workflow_id_fk": { + "name": "execution_large_value_references_workflow_id_workflow_id_fk", + "tableFrom": "execution_large_value_references", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "execution_large_value_references_key_execution_id_source_pk": { + "name": "execution_large_value_references_key_execution_id_source_pk", + "columns": ["key", "execution_id", "source"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_values": { + "name": "execution_large_values", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_execution_id": { + "name": "owner_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "execution_large_values_owner_execution_id_idx": { + "name": "execution_large_values_owner_execution_id_idx", + "columns": [ + { + "expression": "owner_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_cleanup_idx": { + "name": "execution_large_values_cleanup_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"execution_large_values\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_tombstone_cleanup_idx": { + "name": "execution_large_values_tombstone_cleanup_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"execution_large_values\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_values_workspace_id_workspace_id_fk": { + "name": "execution_large_values_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_values", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_large_values_workflow_id_workflow_id_fk": { + "name": "execution_large_values_workflow_id_workflow_id_fk", + "tableFrom": "execution_large_values", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.idempotency_key": { + "name": "idempotency_key", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "result": { + "name": "result", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idempotency_key_created_at_idx": { + "name": "idempotency_key_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "invitation_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'organization'" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "membership_intent": { + "name": "membership_intent", + "type": "invitation_membership_intent", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'internal'" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_organization_id_idx": { + "name": "invitation_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_status_idx": { + "name": "invitation_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_pending_email_org_unique": { + "name": "invitation_pending_email_org_unique", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"invitation\".\"status\" = 'pending' AND \"invitation\".\"organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "invitation_token_unique": { + "name": "invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation_workspace_grant": { + "name": "invitation_workspace_grant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "invitation_id": { + "name": "invitation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_workspace_grant_unique": { + "name": "invitation_workspace_grant_unique", + "columns": [ + { + "expression": "invitation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_workspace_grant_workspace_id_idx": { + "name": "invitation_workspace_grant_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_workspace_grant_invitation_id_invitation_id_fk": { + "name": "invitation_workspace_grant_invitation_id_invitation_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "invitation", + "columnsFrom": ["invitation_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_workspace_grant_workspace_id_workspace_id_fk": { + "name": "invitation_workspace_grant_workspace_id_workspace_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job_execution_logs": { + "name": "job_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "schedule_id": { + "name": "schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "job_execution_logs_schedule_id_idx": { + "name": "job_execution_logs_schedule_id_idx", + "columns": [ + { + "expression": "schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_workspace_started_at_idx": { + "name": "job_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_workspace_ended_at_id_idx": { + "name": "job_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_execution_id_unique": { + "name": "job_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_trigger_idx": { + "name": "job_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "job_execution_logs_schedule_id_workflow_schedule_id_fk": { + "name": "job_execution_logs_schedule_id_workflow_schedule_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workflow_schedule", + "columnsFrom": ["schedule_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "job_execution_logs_workspace_id_workspace_id_fk": { + "name": "job_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base": { + "name": "knowledge_base", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "embedding_dimension": { + "name": "embedding_dimension", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1536 + }, + "chunking_config": { + "name": "chunking_config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"maxSize\": 1024, \"minSize\": 1, \"overlap\": 200}'" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_user_id_idx": { + "name": "kb_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_id_idx": { + "name": "kb_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_user_workspace_idx": { + "name": "kb_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_deleted_at_idx": { + "name": "kb_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_deleted_partial_idx": { + "name": "kb_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_base\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_name_active_unique": { + "name": "kb_workspace_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"knowledge_base\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_user_id_user_id_fk": { + "name": "knowledge_base_user_id_user_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_workspace_id_workspace_id_fk": { + "name": "knowledge_base_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base_tag_definitions": { + "name": "knowledge_base_tag_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_slot": { + "name": "tag_slot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_type": { + "name": "field_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_tag_definitions_kb_slot_idx": { + "name": "kb_tag_definitions_kb_slot_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag_slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_display_name_idx": { + "name": "kb_tag_definitions_kb_display_name_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_id_idx": { + "name": "kb_tag_definitions_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_base_tag_definitions", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_connector": { + "name": "knowledge_connector", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connector_type": { + "name": "connector_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_config": { + "name": "source_config", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "sync_mode": { + "name": "sync_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'full'" + }, + "sync_interval_minutes": { + "name": "sync_interval_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1440 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_sync_error": { + "name": "last_sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_sync_doc_count": { + "name": "last_sync_doc_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "next_sync_at": { + "name": "next_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "consecutive_failures": { + "name": "consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kc_knowledge_base_id_idx": { + "name": "kc_knowledge_base_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_status_next_sync_idx": { + "name": "kc_status_next_sync_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_archived_at_partial_idx": { + "name": "kc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_deleted_at_partial_idx": { + "name": "kc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_connector_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_connector", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_connector_sync_log": { + "name": "knowledge_connector_sync_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "docs_added": { + "name": "docs_added", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_updated": { + "name": "docs_updated", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_deleted": { + "name": "docs_deleted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_unchanged": { + "name": "docs_unchanged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_failed": { + "name": "docs_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kcsl_connector_id_idx": { + "name": "kcsl_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk": { + "name": "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk", + "tableFrom": "knowledge_connector_sync_log", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_server_oauth": { + "name": "mcp_server_oauth", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_server_id": { + "name": "mcp_server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_information": { + "name": "client_information", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokens": { + "name": "tokens", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_verifier": { + "name": "code_verifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_created_at": { + "name": "state_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_refreshed_at": { + "name": "last_refreshed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_server_oauth_server_unique": { + "name": "mcp_server_oauth_server_unique", + "columns": [ + { + "expression": "mcp_server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_server_oauth_state_idx": { + "name": "mcp_server_oauth_state_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk": { + "name": "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "mcp_servers", + "columnsFrom": ["mcp_server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_server_oauth_user_id_user_id_fk": { + "name": "mcp_server_oauth_user_id_user_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_server_oauth_workspace_id_workspace_id_fk": { + "name": "mcp_server_oauth_workspace_id_workspace_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_servers": { + "name": "mcp_servers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transport": { + "name": "transport", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'headers'" + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_client_secret": { + "name": "oauth_client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "headers": { + "name": "headers", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "timeout": { + "name": "timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30000 + }, + "retries": { + "name": "retries", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_connected": { + "name": "last_connected", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "connection_status": { + "name": "connection_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'disconnected'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_config": { + "name": "status_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "tool_count": { + "name": "tool_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_tools_refresh": { + "name": "last_tools_refresh", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_requests": { + "name": "total_requests", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_servers_workspace_enabled_idx": { + "name": "mcp_servers_workspace_enabled_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_workspace_deleted_partial_idx": { + "name": "mcp_servers_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"mcp_servers\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_servers_workspace_id_workspace_id_fk": { + "name": "mcp_servers_workspace_id_workspace_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_servers_created_by_user_id_fk": { + "name": "mcp_servers_created_by_user_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "member_user_id_unique": { + "name": "member_user_id_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_organization_id_idx": { + "name": "member_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory": { + "name": "memory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "memory_key_idx": { + "name": "memory_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_idx": { + "name": "memory_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_key_idx": { + "name": "memory_workspace_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_deleted_partial_idx": { + "name": "memory_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"memory\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_workspace_id_workspace_id_fk": { + "name": "memory_workspace_id_workspace_id_fk", + "tableFrom": "memory", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_allowed_sender": { + "name": "mothership_inbox_allowed_sender", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "added_by": { + "name": "added_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "inbox_sender_ws_email_idx": { + "name": "inbox_sender_ws_email_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_allowed_sender_added_by_user_id_fk": { + "name": "mothership_inbox_allowed_sender_added_by_user_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "user", + "columnsFrom": ["added_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_task": { + "name": "mothership_inbox_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_email": { + "name": "from_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_name": { + "name": "from_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_preview": { + "name": "body_preview", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_text": { + "name": "body_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_html": { + "name": "body_html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_message_id": { + "name": "email_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "in_reply_to": { + "name": "in_reply_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_message_id": { + "name": "response_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agentmail_message_id": { + "name": "agentmail_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'received'" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "trigger_job_id": { + "name": "trigger_job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rejection_reason": { + "name": "rejection_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "has_attachments": { + "name": "has_attachments", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cc_recipients": { + "name": "cc_recipients", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "inbox_task_ws_created_at_idx": { + "name": "inbox_task_ws_created_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_ws_status_idx": { + "name": "inbox_task_ws_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_response_msg_id_idx": { + "name": "inbox_task_response_msg_id_idx", + "columns": [ + { + "expression": "response_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_email_msg_id_idx": { + "name": "inbox_task_email_msg_id_idx", + "columns": [ + { + "expression": "email_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_task_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_task_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_task_chat_id_copilot_chats_id_fk": { + "name": "mothership_inbox_task_chat_id_copilot_chats_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_webhook": { + "name": "mothership_inbox_webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "webhook_id": { + "name": "webhook_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mothership_inbox_webhook_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_webhook_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_webhook", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mothership_inbox_webhook_workspace_id_unique": { + "name": "mothership_inbox_webhook_workspace_id_unique", + "nullsNotDistinct": false, + "columns": ["workspace_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_settings": { + "name": "mothership_settings", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_tool_refs": { + "name": "mcp_tool_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "custom_tool_refs": { + "name": "custom_tool_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "skill_refs": { + "name": "skill_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mothership_settings_workspace_id_idx": { + "name": "mothership_settings_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_settings_workspace_id_workspace_id_fk": { + "name": "mothership_settings_workspace_id_workspace_id_fk", + "tableFrom": "mothership_settings", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "session_policy_settings": { + "name": "session_policy_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "security_policy_version": { + "name": "security_policy_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "whitelabel_settings": { + "name": "whitelabel_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "data_retention_settings": { + "name": "data_retention_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "org_usage_limit": { + "name": "org_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "limit_notifications": { + "name": "limit_notifications", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "departed_member_usage": { + "name": "departed_member_usage", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_member_usage_limit": { + "name": "organization_member_usage_limit", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "usage_limit": { + "name": "usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "set_by": { + "name": "set_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "org_member_usage_limit_org_user_unique": { + "name": "org_member_usage_limit_org_user_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "org_member_usage_limit_organization_id_idx": { + "name": "org_member_usage_limit_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_member_usage_limit_organization_id_organization_id_fk": { + "name": "organization_member_usage_limit_organization_id_organization_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_member_usage_limit_user_id_user_id_fk": { + "name": "organization_member_usage_limit_user_id_user_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_member_usage_limit_set_by_user_id_fk": { + "name": "organization_member_usage_limit_set_by_user_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "user", + "columnsFrom": ["set_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.outbox_event": { + "name": "outbox_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + }, + "available_at": { + "name": "available_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "locked_at": { + "name": "locked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "outbox_event_status_available_idx": { + "name": "outbox_event_status_available_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbox_event_locked_at_idx": { + "name": "outbox_event_locked_at_idx", + "columns": [ + { + "expression": "locked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbox_event_type_created_idx": { + "name": "outbox_event_type_created_idx", + "columns": [ + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.paused_executions": { + "name": "paused_executions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_snapshot": { + "name": "execution_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "pause_points": { + "name": "pause_points", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "total_pause_count": { + "name": "total_pause_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "resumed_count": { + "name": "resumed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "automatic_resume_retry_count": { + "name": "automatic_resume_retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paused'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_resume_at": { + "name": "next_resume_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "paused_executions_workflow_id_idx": { + "name": "paused_executions_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_status_idx": { + "name": "paused_executions_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_execution_id_unique": { + "name": "paused_executions_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_next_resume_at_idx": { + "name": "paused_executions_next_resume_at_idx", + "columns": [ + { + "expression": "next_resume_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'paused' AND next_resume_at IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "paused_executions_workflow_id_workflow_id_fk": { + "name": "paused_executions_workflow_id_workflow_id_fk", + "tableFrom": "paused_executions", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_credential_draft": { + "name": "pending_credential_draft", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pending_draft_user_provider_ws": { + "name": "pending_draft_user_provider_ws", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pending_credential_draft_user_id_user_id_fk": { + "name": "pending_credential_draft_user_id_user_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_workspace_id_workspace_id_fk": { + "name": "pending_credential_draft_workspace_id_workspace_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_credential_id_credential_id_fk": { + "name": "pending_credential_draft_credential_id_credential_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group": { + "name": "permission_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "permission_group_created_by_idx": { + "name": "permission_group_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_organization_name_unique": { + "name": "permission_group_organization_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_organization_default_unique": { + "name": "permission_group_organization_default_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "is_default = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_organization_id_organization_id_fk": { + "name": "permission_group_organization_id_organization_id_fk", + "tableFrom": "permission_group", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_created_by_user_id_fk": { + "name": "permission_group_created_by_user_id_fk", + "tableFrom": "permission_group", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_member": { + "name": "permission_group_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assigned_by": { + "name": "assigned_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_member_group_id_idx": { + "name": "permission_group_member_group_id_idx", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_group_user_unique": { + "name": "permission_group_member_group_user_unique", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_organization_user_idx": { + "name": "permission_group_member_organization_user_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_member_permission_group_id_permission_group_id_fk": { + "name": "permission_group_member_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_organization_id_organization_id_fk": { + "name": "permission_group_member_organization_id_organization_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_user_id_user_id_fk": { + "name": "permission_group_member_user_id_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_assigned_by_user_id_fk": { + "name": "permission_group_member_assigned_by_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["assigned_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_workspace": { + "name": "permission_group_workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_workspace_workspace_id_idx": { + "name": "permission_group_workspace_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_workspace_group_workspace_unique": { + "name": "permission_group_workspace_group_workspace_unique", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_workspace_permission_group_id_permission_group_id_fk": { + "name": "permission_group_workspace_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_workspace_workspace_id_workspace_id_fk": { + "name": "permission_group_workspace_workspace_id_workspace_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_workspace_organization_id_organization_id_fk": { + "name": "permission_group_workspace_organization_id_organization_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permissions": { + "name": "permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_type": { + "name": "permission_type", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permissions_user_id_idx": { + "name": "permissions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_entity_idx": { + "name": "permissions_entity_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_type_idx": { + "name": "permissions_user_entity_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_permission_idx": { + "name": "permissions_user_entity_permission_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_idx": { + "name": "permissions_user_entity_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_unique_constraint": { + "name": "permissions_unique_constraint", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permissions_user_id_user_id_fk": { + "name": "permissions_user_id_user_id_fk", + "tableFrom": "permissions", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.public_share": { + "name": "public_share", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "public_share_token_unique": { + "name": "public_share_token_unique", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_resource_unique": { + "name": "public_share_resource_unique", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_resource_id_idx": { + "name": "public_share_resource_id_idx", + "columns": [ + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_workspace_id_idx": { + "name": "public_share_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "public_share_workspace_id_workspace_id_fk": { + "name": "public_share_workspace_id_workspace_id_fk", + "tableFrom": "public_share", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "public_share_created_by_user_id_fk": { + "name": "public_share_created_by_user_id_fk", + "tableFrom": "public_share", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rate_limit_bucket": { + "name": "rate_limit_bucket", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tokens": { + "name": "tokens", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resume_queue": { + "name": "resume_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "paused_execution_id": { + "name": "paused_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_execution_id": { + "name": "parent_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "new_execution_id": { + "name": "new_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context_id": { + "name": "context_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resume_input": { + "name": "resume_input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "resume_queue_parent_status_idx": { + "name": "resume_queue_parent_status_idx", + "columns": [ + { + "expression": "parent_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resume_queue_new_execution_idx": { + "name": "resume_queue_new_execution_idx", + "columns": [ + { + "expression": "new_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resume_queue_paused_execution_id_paused_executions_id_fk": { + "name": "resume_queue_paused_execution_id_paused_executions_id_fk", + "tableFrom": "resume_queue", + "tableTo": "paused_executions", + "columnsFrom": ["paused_execution_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_user_id_idx": { + "name": "session_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_token_idx": { + "name": "session_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_active_organization_id_organization_id_fk": { + "name": "session_active_organization_id_organization_id_fk", + "tableFrom": "session", + "tableTo": "organization", + "columnsFrom": ["active_organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "auto_connect": { + "name": "auto_connect", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "telemetry_enabled": { + "name": "telemetry_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "email_preferences": { + "name": "email_preferences", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "billing_usage_notifications_enabled": { + "name": "billing_usage_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_training_controls": { + "name": "show_training_controls", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "super_user_mode_enabled": { + "name": "super_user_mode_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "mothership_environment": { + "name": "mothership_environment", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "error_notifications_enabled": { + "name": "error_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "snap_to_grid_size": { + "name": "snap_to_grid_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "show_action_bar": { + "name": "show_action_bar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "copilot_enabled_models": { + "name": "copilot_enabled_models", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "copilot_auto_allowed_tools": { + "name": "copilot_auto_allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_active_workspace_id": { + "name": "last_active_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "settings_user_id_user_id_fk": { + "name": "settings_user_id_user_id_fk", + "tableFrom": "settings", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "settings_user_id_unique": { + "name": "settings_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sim_trigger_state": { + "name": "sim_trigger_state", + "schema": "", + "columns": { + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_key": { + "name": "scope_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sim_trigger_state_workflow_id_workflow_id_fk": { + "name": "sim_trigger_state_workflow_id_workflow_id_fk", + "tableFrom": "sim_trigger_state", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sim_trigger_state_workflow_id_block_id_scope_key_pk": { + "name": "sim_trigger_state_workflow_id_block_id_scope_key_pk", + "columns": ["workflow_id", "block_id", "scope_key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skill": { + "name": "skill", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "skill_workspace_name_unique": { + "name": "skill_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skill_workspace_id_workspace_id_fk": { + "name": "skill_workspace_id_workspace_id_fk", + "tableFrom": "skill", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_user_id_user_id_fk": { + "name": "skill_user_id_user_id_fk", + "tableFrom": "skill", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skill_member": { + "name": "skill_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "skill_id": { + "name": "skill_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "skill_member_user_id_idx": { + "name": "skill_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "skill_member_unique": { + "name": "skill_member_unique", + "columns": [ + { + "expression": "skill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skill_member_skill_id_skill_id_fk": { + "name": "skill_member_skill_id_skill_id_fk", + "tableFrom": "skill_member", + "tableTo": "skill", + "columnsFrom": ["skill_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_member_user_id_user_id_fk": { + "name": "skill_member_user_id_user_id_fk", + "tableFrom": "skill_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_member_invited_by_user_id_fk": { + "name": "skill_member_invited_by_user_id_fk", + "tableFrom": "skill_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_provider": { + "name": "sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sso_provider_provider_id_idx": { + "name": "sso_provider_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_domain_idx": { + "name": "sso_provider_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_user_id_idx": { + "name": "sso_provider_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_organization_id_idx": { + "name": "sso_provider_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sso_provider_user_id_user_id_fk": { + "name": "sso_provider_user_id_user_id_fk", + "tableFrom": "sso_provider", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sso_provider_organization_id_organization_id_fk": { + "name": "sso_provider_organization_id_organization_id_fk", + "tableFrom": "sso_provider", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "period_start": { + "name": "period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "period_end": { + "name": "period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "cancel_at": { + "name": "cancel_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "seats": { + "name": "seats", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "trial_start": { + "name": "trial_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trial_end": { + "name": "trial_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_interval": { + "name": "billing_interval", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_schedule_id": { + "name": "stripe_schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "subscription_reference_status_idx": { + "name": "subscription_reference_status_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "check_enterprise_metadata": { + "name": "check_enterprise_metadata", + "value": "plan != 'enterprise' OR metadata IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.table_jobs": { + "name": "table_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "rows_processed": { + "name": "rows_processed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "table_jobs_one_active_per_table": { + "name": "table_jobs_one_active_per_table", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"table_jobs\".\"status\" = 'running' AND \"table_jobs\".\"type\" <> 'export'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_jobs_watchdog_idx": { + "name": "table_jobs_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_jobs_table_started_idx": { + "name": "table_jobs_table_started_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_jobs_table_id_user_table_definitions_id_fk": { + "name": "table_jobs_table_id_user_table_definitions_id_fk", + "tableFrom": "table_jobs", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_jobs_workspace_id_workspace_id_fk": { + "name": "table_jobs_workspace_id_workspace_id_fk", + "tableFrom": "table_jobs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_row_executions": { + "name": "table_row_executions", + "schema": "", + "columns": { + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_id": { + "name": "job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "running_block_ids": { + "name": "running_block_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "block_errors": { + "name": "block_errors", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enrichment_details": { + "name": "enrichment_details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "table_row_executions_table_status_idx": { + "name": "table_row_executions_table_status_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"status\" IN ('queued', 'running', 'pending')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_execution_id_idx": { + "name": "table_row_executions_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"execution_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_table_group_idx": { + "name": "table_row_executions_table_group_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_row_executions_table_id_user_table_definitions_id_fk": { + "name": "table_row_executions_table_id_user_table_definitions_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_row_executions_row_id_user_table_rows_id_fk": { + "name": "table_row_executions_row_id_user_table_rows_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_rows", + "columnsFrom": ["row_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "table_row_executions_row_id_group_id_pk": { + "name": "table_row_executions_row_id_group_id_pk", + "columns": ["row_id", "group_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_run_dispatches": { + "name": "table_run_dispatches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "cursor": { + "name": "cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "limit": { + "name": "limit", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "processed_count": { + "name": "processed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_manual_run": { + "name": "is_manual_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "triggered_by_user_id": { + "name": "triggered_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "table_run_dispatches_active_idx": { + "name": "table_run_dispatches_active_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_run_dispatches_watchdog_idx": { + "name": "table_run_dispatches_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_run_dispatches_table_id_user_table_definitions_id_fk": { + "name": "table_run_dispatches_table_id_user_table_definitions_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_run_dispatches_workspace_id_workspace_id_fk": { + "name": "table_run_dispatches_workspace_id_workspace_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_run_dispatches_triggered_by_user_id_user_id_fk": { + "name": "table_run_dispatches_triggered_by_user_id_user_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user", + "columnsFrom": ["triggered_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.usage_log": { + "name": "usage_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "usage_log_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "usage_log_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_entity_type": { + "name": "billing_entity_type", + "type": "billing_entity_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "billing_entity_id": { + "name": "billing_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_period_start": { + "name": "billing_period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_period_end": { + "name": "billing_period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "usage_log_user_created_at_idx": { + "name": "usage_log_user_created_at_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_source_idx": { + "name": "usage_log_source_idx", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_id_idx": { + "name": "usage_log_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workflow_id_idx": { + "name": "usage_log_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_event_key_unique": { + "name": "usage_log_event_key_unique", + "columns": [ + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"usage_log\".\"event_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_billing_entity_period_idx": { + "name": "usage_log_billing_entity_period_idx", + "columns": [ + { + "expression": "billing_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_end", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_created_at_idx": { + "name": "usage_log_workspace_created_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_execution_id_idx": { + "name": "usage_log_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "usage_log_user_id_user_id_fk": { + "name": "usage_log_user_id_user_id_fk", + "tableFrom": "usage_log", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "usage_log_workspace_id_workspace_id_fk": { + "name": "usage_log_workspace_id_workspace_id_fk", + "tableFrom": "usage_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "usage_log_workflow_id_workflow_id_fk": { + "name": "usage_log_workflow_id_workflow_id_fk", + "tableFrom": "usage_log", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "usage_log_billing_scope_all_or_none": { + "name": "usage_log_billing_scope_all_or_none", + "value": "(\n (\"usage_log\".\"billing_entity_type\" IS NULL AND \"usage_log\".\"billing_entity_id\" IS NULL AND \"usage_log\".\"billing_period_start\" IS NULL AND \"usage_log\".\"billing_period_end\" IS NULL)\n OR\n (\"usage_log\".\"billing_entity_type\" IS NOT NULL AND \"usage_log\".\"billing_entity_id\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" IS NOT NULL AND \"usage_log\".\"billing_period_end\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" < \"usage_log\".\"billing_period_end\")\n )" + } + }, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "normalized_email": { + "name": "normalized_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'user'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + }, + "user_normalized_email_unique": { + "name": "user_normalized_email_unique", + "nullsNotDistinct": false, + "columns": ["normalized_email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_stats": { + "name": "user_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "total_manual_executions": { + "name": "total_manual_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_api_calls": { + "name": "total_api_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_webhook_triggers": { + "name": "total_webhook_triggers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_scheduled_executions": { + "name": "total_scheduled_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_chat_executions": { + "name": "total_chat_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_executions": { + "name": "total_mcp_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_tokens_used": { + "name": "total_tokens_used", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost": { + "name": "total_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_usage_limit": { + "name": "current_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'5'" + }, + "usage_limit_updated_at": { + "name": "usage_limit_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "current_period_cost": { + "name": "current_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_cost": { + "name": "last_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "billed_overage_this_period": { + "name": "billed_overage_this_period", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "pro_period_cost_snapshot": { + "name": "pro_period_cost_snapshot", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "pro_period_cost_snapshot_at": { + "name": "pro_period_cost_snapshot_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "total_copilot_cost": { + "name": "total_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_copilot_cost": { + "name": "current_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_copilot_cost": { + "name": "last_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "total_copilot_tokens": { + "name": "total_copilot_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_copilot_calls": { + "name": "total_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_calls": { + "name": "total_mcp_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_cost": { + "name": "total_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_mcp_copilot_cost": { + "name": "current_period_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_active": { + "name": "last_active", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "billing_blocked": { + "name": "billing_blocked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "billing_blocked_reason": { + "name": "billing_blocked_reason", + "type": "billing_blocked_reason", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "limit_notifications": { + "name": "limit_notifications", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": { + "user_stats_user_id_user_id_fk": { + "name": "user_stats_user_id_user_id_fk", + "tableFrom": "user_stats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_stats_user_id_unique": { + "name": "user_stats_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_definitions": { + "name": "user_table_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema": { + "name": "schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "max_rows": { + "name": "max_rows", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10000 + }, + "row_count": { + "name": "row_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "rows_version": { + "name": "rows_version", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_table_def_workspace_id_idx": { + "name": "user_table_def_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_name_unique": { + "name": "user_table_def_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_table_definitions\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_archived_at_idx": { + "name": "user_table_def_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_archived_partial_idx": { + "name": "user_table_def_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_table_definitions\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_definitions_workspace_id_workspace_id_fk": { + "name": "user_table_definitions_workspace_id_workspace_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_definitions_created_by_user_id_fk": { + "name": "user_table_definitions_created_by_user_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_rows": { + "name": "user_table_rows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "order_key": { + "name": "order_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_table_rows_tenant_data_gin_idx": { + "name": "user_table_rows_tenant_data_gin_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"data\" jsonb_path_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "user_table_rows_workspace_table_idx": { + "name": "user_table_rows_workspace_table_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_position_idx": { + "name": "user_table_rows_table_position_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_order_key_idx": { + "name": "user_table_rows_table_order_key_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "order_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_id_id_idx": { + "name": "user_table_rows_table_id_id_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_rows_table_id_user_table_definitions_id_fk": { + "name": "user_table_rows_table_id_user_table_definitions_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_workspace_id_workspace_id_fk": { + "name": "user_table_rows_workspace_id_workspace_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_created_by_user_id_fk": { + "name": "user_table_rows_created_by_user_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "verification_expires_at_idx": { + "name": "verification_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.waitlist": { + "name": "waitlist", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "waitlist_email_unique": { + "name": "waitlist_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook": { + "name": "webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registration_status": { + "name": "registration_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registration_generation": { + "name": "registration_generation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "config_fingerprint": { + "name": "config_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prepared_at": { + "name": "prepared_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "routing_key": { + "name": "routing_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_config": { + "name": "provider_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "path_deployment_unique": { + "name": "path_deployment_unique", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"webhook\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_workflow_deployment_idx": { + "name": "webhook_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_routing_key_active_idx": { + "name": "webhook_routing_key_active_idx", + "columns": [ + { + "expression": "routing_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook\".\"archived_at\" IS NULL AND \"webhook\".\"routing_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_archived_at_partial_idx": { + "name": "webhook_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468": { + "name": "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_tiktok_credential_id_idx": { + "name": "webhook_tiktok_credential_id_idx", + "columns": [ + { + "expression": "((\"provider_config\")::jsonb ->> 'credentialId')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook\".\"provider\" = 'tiktok' AND \"webhook\".\"is_active\" = true AND \"webhook\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_workflow_id_block_id_updated_at_desc": { + "name": "idx_webhook_on_workflow_id_block_id_updated_at_desc", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_active_registration_unique": { + "name": "webhook_active_registration_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"webhook\".\"registration_status\" = 'active' AND \"webhook\".\"block_id\" IS NOT NULL AND \"webhook\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_candidate_registration_unique": { + "name": "webhook_candidate_registration_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"webhook\".\"registration_status\" = 'candidate' AND \"webhook\".\"block_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_registration_status_generation_idx": { + "name": "webhook_registration_status_generation_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "registration_status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "registration_generation", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_workflow_id_workflow_id_fk": { + "name": "webhook_workflow_id_workflow_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "webhook_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "webhook_registration_status_check": { + "name": "webhook_registration_status_check", + "value": "\"webhook\".\"registration_status\" IS NULL OR \"webhook\".\"registration_status\" IN ('active', 'candidate', 'retired', 'orphaned')" + }, + "webhook_registration_generation_check": { + "name": "webhook_registration_generation_check", + "value": "\"webhook\".\"registration_generation\" IS NULL OR \"webhook\".\"registration_generation\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.webhook_path_claim": { + "name": "webhook_path_claim", + "schema": "", + "columns": { + "path": { + "name": "path", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "webhook_path_claim_workflow_idx": { + "name": "webhook_path_claim_workflow_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_path_claim_workflow_id_workflow_id_fk": { + "name": "webhook_path_claim_workflow_id_workflow_id_fk", + "tableFrom": "webhook_path_claim", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "webhook_path_claim_generation_check": { + "name": "webhook_path_claim_generation_check", + "value": "\"webhook_path_claim\".\"generation\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_synced": { + "name": "last_synced", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "is_deployed": { + "name": "is_deployed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deployed_at": { + "name": "deployed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "is_public_api": { + "name": "is_public_api", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "fork_sync_excluded": { + "name": "fork_sync_excluded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_user_id_idx": { + "name": "workflow_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_id_idx": { + "name": "workflow_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_user_workspace_idx": { + "name": "workflow_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_folder_name_active_unique": { + "name": "workflow_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_sort_idx": { + "name": "workflow_folder_sort_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_archived_at_idx": { + "name": "workflow_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_archived_partial_idx": { + "name": "workflow_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_user_id_user_id_fk": { + "name": "workflow_user_id_user_id_fk", + "tableFrom": "workflow", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_workspace_id_workspace_id_fk": { + "name": "workflow_workspace_id_workspace_id_fk", + "tableFrom": "workflow", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_id_workflow_folder_id_fk": { + "name": "workflow_folder_id_workflow_folder_id_fk", + "tableFrom": "workflow", + "tableTo": "workflow_folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_blocks": { + "name": "workflow_blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position_x": { + "name": "position_x", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "position_y": { + "name": "position_y", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "horizontal_handles": { + "name": "horizontal_handles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_wide": { + "name": "is_wide", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "advanced_mode": { + "name": "advanced_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trigger_mode": { + "name": "trigger_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "sub_blocks": { + "name": "sub_blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_blocks_workflow_id_idx": { + "name": "workflow_blocks_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_type_idx": { + "name": "workflow_blocks_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_blocks_workflow_id_workflow_id_fk": { + "name": "workflow_blocks_workflow_id_workflow_id_fk", + "tableFrom": "workflow_blocks", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_checkpoints": { + "name": "workflow_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_state": { + "name": "workflow_state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_checkpoints_user_id_idx": { + "name": "workflow_checkpoints_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_id_idx": { + "name": "workflow_checkpoints_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_id_idx": { + "name": "workflow_checkpoints_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_message_id_idx": { + "name": "workflow_checkpoints_message_id_idx", + "columns": [ + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_user_workflow_idx": { + "name": "workflow_checkpoints_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_chat_idx": { + "name": "workflow_checkpoints_workflow_chat_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_created_at_idx": { + "name": "workflow_checkpoints_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_created_at_idx": { + "name": "workflow_checkpoints_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_checkpoints_user_id_user_id_fk": { + "name": "workflow_checkpoints_user_id_user_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_workflow_id_workflow_id_fk": { + "name": "workflow_checkpoints_workflow_id_workflow_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_chat_id_copilot_chats_id_fk": { + "name": "workflow_checkpoints_chat_id_copilot_chats_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_deployment_operation": { + "name": "workflow_deployment_operation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "previous_active_version_id": { + "name": "previous_active_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "protocol_version": { + "name": "protocol_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'preparing'" + }, + "component_readiness": { + "name": "component_readiness", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_deployment_operation_workflow_generation_unique": { + "name": "workflow_deployment_operation_workflow_generation_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "generation", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_workflow_idempotency_unique": { + "name": "workflow_deployment_operation_workflow_idempotency_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_deployment_operation\".\"idempotency_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_workflow_in_flight_unique": { + "name": "workflow_deployment_operation_workflow_in_flight_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_deployment_operation\".\"status\" IN ('preparing', 'activating')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_workflow_status_idx": { + "name": "workflow_deployment_operation_workflow_status_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_deployment_version_idx": { + "name": "workflow_deployment_operation_deployment_version_idx", + "columns": [ + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_workflow_version_generation_idx": { + "name": "workflow_deployment_operation_workflow_version_generation_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "generation", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_deployment_operation_workflow_id_workflow_id_fk": { + "name": "workflow_deployment_operation_workflow_id_workflow_id_fk", + "tableFrom": "workflow_deployment_operation", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_deployment_operation_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_deployment_operation_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_deployment_operation", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_deployment_operation_previous_active_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_deployment_operation_previous_active_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_deployment_operation", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["previous_active_version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workflow_deployment_operation_action_check": { + "name": "workflow_deployment_operation_action_check", + "value": "\"workflow_deployment_operation\".\"action\" IN ('deploy', 'activate')" + }, + "workflow_deployment_operation_status_check": { + "name": "workflow_deployment_operation_status_check", + "value": "\"workflow_deployment_operation\".\"status\" IN ('preparing', 'activating', 'active', 'failed', 'superseded')" + }, + "workflow_deployment_operation_generation_check": { + "name": "workflow_deployment_operation_generation_check", + "value": "\"workflow_deployment_operation\".\"generation\" > 0" + }, + "workflow_deployment_operation_protocol_version_check": { + "name": "workflow_deployment_operation_protocol_version_check", + "value": "\"workflow_deployment_operation\".\"protocol_version\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.workflow_deployment_version": { + "name": "workflow_deployment_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_deployment_version_workflow_version_unique": { + "name": "workflow_deployment_version_workflow_version_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_workflow_active_idx": { + "name": "workflow_deployment_version_workflow_active_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_created_at_idx": { + "name": "workflow_deployment_version_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_deployment_version_workflow_id_workflow_id_fk": { + "name": "workflow_deployment_version_workflow_id_workflow_id_fk", + "tableFrom": "workflow_deployment_version", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_edges": { + "name": "workflow_edges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_block_id": { + "name": "source_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_handle": { + "name": "source_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_handle": { + "name": "target_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_edges_workflow_id_idx": { + "name": "workflow_edges_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_source_idx": { + "name": "workflow_edges_workflow_source_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_target_idx": { + "name": "workflow_edges_workflow_target_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_edges_workflow_id_workflow_id_fk": { + "name": "workflow_edges_workflow_id_workflow_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_source_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_source_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["source_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_target_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_target_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["target_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_logs": { + "name": "workflow_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_snapshot_id": { + "name": "state_snapshot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost_total": { + "name": "cost_total", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "models_used": { + "name": "models_used", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "files": { + "name": "files", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_execution_logs_workflow_id_idx": { + "name": "workflow_execution_logs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_state_snapshot_id_idx": { + "name": "workflow_execution_logs_state_snapshot_id_idx", + "columns": [ + { + "expression": "state_snapshot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_deployment_version_id_idx": { + "name": "workflow_execution_logs_deployment_version_id_idx", + "columns": [ + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_trigger_idx": { + "name": "workflow_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_level_idx": { + "name": "workflow_execution_logs_level_idx", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_started_at_idx": { + "name": "workflow_execution_logs_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_execution_id_unique": { + "name": "workflow_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workflow_started_at_idx": { + "name": "workflow_execution_logs_workflow_started_at_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_idx": { + "name": "workflow_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_id_desc_idx": { + "name": "workflow_execution_logs_workspace_started_at_id_desc_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"started_at\" DESC NULLS LAST", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "\"id\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_cost_total_idx": { + "name": "workflow_execution_logs_workspace_cost_total_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_total", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_models_used_idx": { + "name": "workflow_execution_logs_models_used_idx", + "columns": [ + { + "expression": "models_used", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "workflow_execution_logs_workspace_ended_at_id_idx": { + "name": "workflow_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_running_started_at_idx": { + "name": "workflow_execution_logs_running_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_completed_ended_at_idx": { + "name": "workflow_execution_logs_completed_ended_at_idx", + "columns": [ + { + "expression": "ended_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_execution_logs\".\"status\" = 'completed' AND \"workflow_execution_logs\".\"level\" = 'info' AND \"workflow_execution_logs\".\"ended_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_logs_workflow_id_workflow_id_fk": { + "name": "workflow_execution_logs_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workflow_execution_logs_workspace_id_workspace_id_fk": { + "name": "workflow_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk": { + "name": "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_execution_snapshots", + "columnsFrom": ["state_snapshot_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_snapshots": { + "name": "workflow_execution_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_data": { + "name": "state_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_snapshots_workflow_id_idx": { + "name": "workflow_snapshots_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_hash_idx": { + "name": "workflow_snapshots_hash_idx", + "columns": [ + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_workflow_hash_idx": { + "name": "workflow_snapshots_workflow_hash_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_created_at_idx": { + "name": "workflow_snapshots_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_snapshots_workflow_id_workflow_id_fk": { + "name": "workflow_execution_snapshots_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_snapshots", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_folder": { + "name": "workflow_folder", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'#6B7280'" + }, + "is_expanded": { + "name": "is_expanded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_folder_user_idx": { + "name": "workflow_folder_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_parent_idx": { + "name": "workflow_folder_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_parent_sort_idx": { + "name": "workflow_folder_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_archived_at_idx": { + "name": "workflow_folder_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_archived_partial_idx": { + "name": "workflow_folder_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_folder\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_folder_user_id_user_id_fk": { + "name": "workflow_folder_user_id_user_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_workspace_id_workspace_id_fk": { + "name": "workflow_folder_workspace_id_workspace_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_server": { + "name": "workflow_mcp_server", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_server_workspace_id_idx": { + "name": "workflow_mcp_server_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_created_by_idx": { + "name": "workflow_mcp_server_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_deleted_at_idx": { + "name": "workflow_mcp_server_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_workspace_deleted_partial_idx": { + "name": "workflow_mcp_server_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_server\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_server_workspace_id_workspace_id_fk": { + "name": "workflow_mcp_server_workspace_id_workspace_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_server_created_by_user_id_fk": { + "name": "workflow_mcp_server_created_by_user_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_tool": { + "name": "workflow_mcp_tool", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_description": { + "name": "tool_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parameter_schema": { + "name": "parameter_schema", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "parameter_description_overrides": { + "name": "parameter_description_overrides", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'::json" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_tool_server_id_idx": { + "name": "workflow_mcp_tool_server_id_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_workflow_id_idx": { + "name": "workflow_mcp_tool_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_server_workflow_unique": { + "name": "workflow_mcp_tool_server_workflow_unique", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_archived_at_partial_idx": { + "name": "workflow_mcp_tool_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk": { + "name": "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow_mcp_server", + "columnsFrom": ["server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_tool_workflow_id_workflow_id_fk": { + "name": "workflow_mcp_tool_workflow_id_workflow_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_schedule": { + "name": "workflow_schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_operation_id": { + "name": "deployment_operation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_ran_at": { + "name": "last_ran_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_queued_at": { + "name": "last_queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "infra_retry_count": { + "name": "infra_retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'workflow'" + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lifecycle": { + "name": "lifecycle", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'persistent'" + }, + "success_condition": { + "name": "success_condition", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "max_runs": { + "name": "max_runs", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "source_chat_id": { + "name": "source_chat_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_task_name": { + "name": "source_task_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_user_id": { + "name": "source_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_workspace_id": { + "name": "source_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_history": { + "name": "job_history", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "contexts": { + "name": "contexts", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "excluded_dates": { + "name": "excluded_dates", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "ends_at": { + "name": "ends_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_schedule_workflow_block_deployment_unique": { + "name": "workflow_schedule_workflow_block_deployment_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_workflow_deployment_idx": { + "name": "workflow_schedule_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_archived_at_partial_idx": { + "name": "workflow_schedule_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6": { + "name": "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6", + "columns": [ + { + "expression": "source_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_workflow_idx": { + "name": "workflow_schedule_due_workflow_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND (\"workflow_schedule\".\"source_type\" = 'workflow' OR \"workflow_schedule\".\"source_type\" IS NULL)", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_job_idx": { + "name": "workflow_schedule_due_job_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND \"workflow_schedule\".\"source_type\" = 'job'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_schedule_workflow_id_workflow_id_fk": { + "name": "workflow_schedule_workflow_id_workflow_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_deployment_operation_id_workflow_deployment_operation_id_fk": { + "name": "workflow_schedule_deployment_operation_id_workflow_deployment_operation_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_deployment_operation", + "columnsFrom": ["deployment_operation_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workflow_schedule_source_user_id_user_id_fk": { + "name": "workflow_schedule_source_user_id_user_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "user", + "columnsFrom": ["source_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_source_workspace_id_workspace_id_fk": { + "name": "workflow_schedule_source_workspace_id_workspace_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workspace", + "columnsFrom": ["source_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_subflows": { + "name": "workflow_subflows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_subflows_workflow_id_idx": { + "name": "workflow_subflows_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_subflows_workflow_type_idx": { + "name": "workflow_subflows_workflow_type_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_subflows_workflow_id_workflow_id_fk": { + "name": "workflow_subflows_workflow_id_workflow_id_fk", + "tableFrom": "workflow_subflows", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace": { + "name": "workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#33C482'" + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_mode": { + "name": "workspace_mode", + "type": "workspace_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'grandfathered_shared'" + }, + "billed_account_user_id": { + "name": "billed_account_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "allow_personal_api_keys": { + "name": "allow_personal_api_keys", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "inbox_enabled": { + "name": "inbox_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "inbox_address": { + "name": "inbox_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inbox_provider_id": { + "name": "inbox_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "organization_assigned_at": { + "name": "organization_assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "forked_from_workspace_id": { + "name": "forked_from_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_owner_id_idx": { + "name": "workspace_owner_id_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_organization_id_idx": { + "name": "workspace_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_mode_idx": { + "name": "workspace_mode_idx", + "columns": [ + { + "expression": "workspace_mode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_forked_from_workspace_id_idx": { + "name": "workspace_forked_from_workspace_id_idx", + "columns": [ + { + "expression": "forked_from_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_owner_id_user_id_fk": { + "name": "workspace_owner_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_organization_id_organization_id_fk": { + "name": "workspace_organization_id_organization_id_fk", + "tableFrom": "workspace", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_billed_account_user_id_user_id_fk": { + "name": "workspace_billed_account_user_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["billed_account_user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workspace_forked_from_workspace_id_workspace_id_fk": { + "name": "workspace_forked_from_workspace_id_workspace_id_fk", + "tableFrom": "workspace", + "tableTo": "workspace", + "columnsFrom": ["forked_from_workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workspace_storage_used_bytes_non_negative": { + "name": "workspace_storage_used_bytes_non_negative", + "value": "\"workspace\".\"storage_used_bytes\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.workspace_byok_keys": { + "name": "workspace_byok_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_byok_workspace_provider_idx": { + "name": "workspace_byok_workspace_provider_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_byok_keys_workspace_id_workspace_id_fk": { + "name": "workspace_byok_keys_workspace_id_workspace_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_byok_keys_created_by_user_id_fk": { + "name": "workspace_byok_keys_created_by_user_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_environment": { + "name": "workspace_environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_environment_workspace_unique": { + "name": "workspace_environment_workspace_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_environment_workspace_id_workspace_id_fk": { + "name": "workspace_environment_workspace_id_workspace_id_fk", + "tableFrom": "workspace_environment", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file": { + "name": "workspace_file", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_workspace_id_idx": { + "name": "workspace_file_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_key_idx": { + "name": "workspace_file_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_deleted_at_idx": { + "name": "workspace_file_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_workspace_deleted_partial_idx": { + "name": "workspace_file_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_workspace_id_workspace_id_fk": { + "name": "workspace_file_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_uploaded_by_user_id_fk": { + "name": "workspace_file_uploaded_by_user_id_fk", + "tableFrom": "workspace_file", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_file_key_unique": { + "name": "workspace_file_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_folders": { + "name": "workspace_file_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_folders_workspace_parent_idx": { + "name": "workspace_file_folders_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_parent_sort_idx": { + "name": "workspace_file_folders_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_deleted_at_idx": { + "name": "workspace_file_folders_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_workspace_deleted_partial_idx": { + "name": "workspace_file_folders_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_folders\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_workspace_parent_name_active_unique": { + "name": "workspace_file_folders_workspace_parent_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"parent_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_file_folders\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_folders_user_id_user_id_fk": { + "name": "workspace_file_folders_user_id_user_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_folders_workspace_id_workspace_id_fk": { + "name": "workspace_file_folders_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_folders_parent_id_workspace_file_folders_id_fk": { + "name": "workspace_file_folders_parent_id_workspace_file_folders_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "workspace_file_folders", + "columnsFrom": ["parent_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_files": { + "name": "workspace_files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context": { + "name": "context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "original_name": { + "name": "original_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_files_key_active_unique": { + "name": "workspace_files_key_active_unique", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_folder_name_active_unique": { + "name": "workspace_files_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "original_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL AND \"workspace_files\".\"context\" = 'workspace' AND \"workspace_files\".\"workspace_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_chat_display_name_unique": { + "name": "workspace_files_chat_display_name_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"context\" = 'mothership' AND \"workspace_files\".\"chat_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_key_idx": { + "name": "workspace_files_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_user_id_idx": { + "name": "workspace_files_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_id_idx": { + "name": "workspace_files_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_folder_id_idx": { + "name": "workspace_files_folder_id_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_context_idx": { + "name": "workspace_files_context_idx", + "columns": [ + { + "expression": "context", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_chat_id_idx": { + "name": "workspace_files_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_deleted_at_idx": { + "name": "workspace_files_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_deleted_partial_idx": { + "name": "workspace_files_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_files\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_files_user_id_user_id_fk": { + "name": "workspace_files_user_id_user_id_fk", + "tableFrom": "workspace_files", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_workspace_id_workspace_id_fk": { + "name": "workspace_files_workspace_id_workspace_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_folder_id_workspace_file_folders_id_fk": { + "name": "workspace_files_folder_id_workspace_file_folders_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace_file_folders", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_files_chat_id_copilot_chats_id_fk": { + "name": "workspace_files_chat_id_copilot_chats_id_fk", + "tableFrom": "workspace_files", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_block_map": { + "name": "workspace_fork_block_map", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_workflow_id": { + "name": "parent_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_block_id": { + "name": "parent_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_workflow_id": { + "name": "child_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_block_id": { + "name": "child_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_block_map_child_ws_parent_unique": { + "name": "workspace_fork_block_map_child_ws_parent_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_child_unique": { + "name": "workspace_fork_block_map_child_ws_child_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_parent_wf_idx": { + "name": "workspace_fork_block_map_child_ws_parent_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_child_wf_idx": { + "name": "workspace_fork_block_map_child_ws_child_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_block_map_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_block_map_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_block_map", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_dependent_value": { + "name": "workspace_fork_dependent_value", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_workflow_id": { + "name": "target_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sub_block_key": { + "name": "sub_block_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_dependent_value_child_ws_wf_idx": { + "name": "workspace_fork_dependent_value_child_ws_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_dependent_value_field_unique": { + "name": "workspace_fork_dependent_value_field_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sub_block_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_dependent_value_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_dependent_value_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_dependent_value", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_promote_run": { + "name": "workspace_fork_promote_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_workspace_id": { + "name": "source_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_workspace_id": { + "name": "target_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "workspace_fork_promote_direction", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "snapshot": { + "name": "snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_promote_run_child_ws_target_unique": { + "name": "workspace_fork_promote_run_child_ws_target_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_promote_run_target_ws_idx": { + "name": "workspace_fork_promote_run_target_ws_idx", + "columns": [ + { + "expression": "target_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_promote_run_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_promote_run_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_promote_run", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_fork_promote_run_created_by_user_id_fk": { + "name": "workspace_fork_promote_run_created_by_user_id_fk", + "tableFrom": "workspace_fork_promote_run", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_resource_map": { + "name": "workspace_fork_resource_map", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "workspace_fork_resource_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "parent_resource_id": { + "name": "parent_resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_resource_id": { + "name": "child_resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_resource_map_child_ws_idx": { + "name": "workspace_fork_resource_map_child_ws_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_resource_map_child_ws_type_idx": { + "name": "workspace_fork_resource_map_child_ws_type_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_resource_map_child_type_parent_unique": { + "name": "workspace_fork_resource_map_child_type_parent_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_resource_map_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_resource_map_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_resource_map", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_fork_resource_map_created_by_user_id_fk": { + "name": "workspace_fork_resource_map_created_by_user_id_fk", + "tableFrom": "workspace_fork_resource_map", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.academy_cert_status": { + "name": "academy_cert_status", + "schema": "public", + "values": ["active", "revoked", "expired"] + }, + "public.background_work_kind": { + "name": "background_work_kind", + "schema": "public", + "values": ["deployment_side_effects", "fork_content_copy", "fork_sync", "fork_rollback"] + }, + "public.background_work_status_value": { + "name": "background_work_status_value", + "schema": "public", + "values": ["pending", "processing", "completed", "completed_with_warnings", "failed"] + }, + "public.billing_blocked_reason": { + "name": "billing_blocked_reason", + "schema": "public", + "values": ["payment_failed", "dispute"] + }, + "public.billing_entity_type": { + "name": "billing_entity_type", + "schema": "public", + "values": ["user", "organization"] + }, + "public.chat_type": { + "name": "chat_type", + "schema": "public", + "values": ["mothership", "copilot"] + }, + "public.copilot_async_tool_status": { + "name": "copilot_async_tool_status", + "schema": "public", + "values": ["pending", "running", "completed", "failed", "cancelled", "delivered"] + }, + "public.copilot_run_status": { + "name": "copilot_run_status", + "schema": "public", + "values": ["active", "paused_waiting_for_tool", "resuming", "complete", "error", "cancelled"] + }, + "public.credential_member_role": { + "name": "credential_member_role", + "schema": "public", + "values": ["admin", "member"] + }, + "public.credential_member_status": { + "name": "credential_member_status", + "schema": "public", + "values": ["active", "pending", "revoked"] + }, + "public.credential_type": { + "name": "credential_type", + "schema": "public", + "values": ["oauth", "env_workspace", "env_personal", "service_account"] + }, + "public.data_drain_cadence": { + "name": "data_drain_cadence", + "schema": "public", + "values": ["hourly", "daily"] + }, + "public.data_drain_destination": { + "name": "data_drain_destination", + "schema": "public", + "values": ["s3", "gcs", "azure_blob", "datadog", "bigquery", "snowflake", "webhook"] + }, + "public.data_drain_run_status": { + "name": "data_drain_run_status", + "schema": "public", + "values": ["running", "success", "failed"] + }, + "public.data_drain_run_trigger": { + "name": "data_drain_run_trigger", + "schema": "public", + "values": ["cron", "manual"] + }, + "public.data_drain_source": { + "name": "data_drain_source", + "schema": "public", + "values": ["workflow_logs", "job_logs", "audit_logs", "copilot_chats", "copilot_runs"] + }, + "public.execution_large_value_reference_source": { + "name": "execution_large_value_reference_source", + "schema": "public", + "values": ["execution_log", "paused_snapshot"] + }, + "public.invitation_kind": { + "name": "invitation_kind", + "schema": "public", + "values": ["organization", "workspace"] + }, + "public.invitation_membership_intent": { + "name": "invitation_membership_intent", + "schema": "public", + "values": ["internal", "external"] + }, + "public.invitation_status": { + "name": "invitation_status", + "schema": "public", + "values": ["pending", "accepted", "rejected", "cancelled", "expired"] + }, + "public.permission_type": { + "name": "permission_type", + "schema": "public", + "values": ["admin", "write", "read"] + }, + "public.usage_log_category": { + "name": "usage_log_category", + "schema": "public", + "values": ["model", "fixed", "tool"] + }, + "public.usage_log_source": { + "name": "usage_log_source", + "schema": "public", + "values": [ + "workflow", + "wand", + "copilot", + "workspace-chat", + "mcp_copilot", + "mothership_block", + "knowledge-base", + "voice-input", + "enrichment" + ] + }, + "public.workspace_fork_promote_direction": { + "name": "workspace_fork_promote_direction", + "schema": "public", + "values": ["push", "pull"] + }, + "public.workspace_fork_resource_type": { + "name": "workspace_fork_resource_type", + "schema": "public", + "values": [ + "workflow", + "oauth_credential", + "service_account_credential", + "env_var", + "table", + "knowledge_base", + "knowledge_document", + "file", + "mcp_server", + "workflow_mcp_server", + "custom_tool", + "skill" + ] + }, + "public.workspace_mode": { + "name": "workspace_mode", + "schema": "public", + "values": ["personal", "organization", "grandfathered_shared"] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index 39b5fac75ac..7ec307a7e73 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -1863,6 +1863,13 @@ "when": 1784838122475, "tag": "0266_spotty_alex_power", "breakpoints": true + }, + { + "idx": 267, + "version": "7", + "when": 1784861110986, + "tag": "0267_dark_romulus", + "breakpoints": true } ] } diff --git a/packages/db/schema.ts b/packages/db/schema.ts index 146284517bd..2badf305690 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -1050,6 +1050,31 @@ export const skill = pgTable( }) ) +/** + * Editor grants for a skill. A row makes the user an editor (edit, delete, + * share); workspace admins are derived editors and need no rows. Everyone with + * workspace access can see and use every skill regardless of rows. + */ +export const skillMember = pgTable( + 'skill_member', + { + id: text('id').primaryKey(), + skillId: text('skill_id') + .notNull() + .references(() => skill.id, { onDelete: 'cascade' }), + userId: text('user_id') + .notNull() + .references(() => user.id, { onDelete: 'cascade' }), + invitedBy: text('invited_by').references(() => user.id, { onDelete: 'set null' }), + createdAt: timestamp('created_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at').notNull().defaultNow(), + }, + (table) => ({ + userIdIdx: index('skill_member_user_id_idx').on(table.userId), + uniqueMembership: uniqueIndex('skill_member_unique').on(table.skillId, table.userId), + }) +) + export const mothershipSettings = pgTable( 'mothership_settings', { diff --git a/packages/emcn/src/components/chip-switch/chip-switch.tsx b/packages/emcn/src/components/chip-switch/chip-switch.tsx index 717af940ba5..cf0cf535de2 100644 --- a/packages/emcn/src/components/chip-switch/chip-switch.tsx +++ b/packages/emcn/src/components/chip-switch/chip-switch.tsx @@ -22,7 +22,7 @@ export interface ChipSwitchOption { */ export interface ChipSwitchProps { /** Ordered list of options to render as segments. */ - options: ChipSwitchOption[] + options: readonly ChipSwitchOption[] /** Currently selected value. */ value: T /** Invoked with the next selection when a segment is clicked. */ diff --git a/packages/testing/src/mocks/audit.mock.ts b/packages/testing/src/mocks/audit.mock.ts index e18095f5d36..356ebc4174d 100644 --- a/packages/testing/src/mocks/audit.mock.ts +++ b/packages/testing/src/mocks/audit.mock.ts @@ -131,6 +131,8 @@ export const auditMock = { SKILL_CREATED: 'skill.created', SKILL_UPDATED: 'skill.updated', SKILL_DELETED: 'skill.deleted', + SKILL_MEMBER_ADDED: 'skill_member.added', + SKILL_MEMBER_REMOVED: 'skill_member.removed', TABLE_CREATED: 'table.created', TABLE_UPDATED: 'table.updated', TABLE_DELETED: 'table.deleted', diff --git a/packages/testing/src/mocks/schema.mock.ts b/packages/testing/src/mocks/schema.mock.ts index 9f892b1c2ec..f7f4084c798 100644 --- a/packages/testing/src/mocks/schema.mock.ts +++ b/packages/testing/src/mocks/schema.mock.ts @@ -406,6 +406,14 @@ export const schemaMock = { createdAt: 'createdAt', updatedAt: 'updatedAt', }, + skillMember: { + id: 'id', + skillId: 'skillId', + userId: 'userId', + invitedBy: 'invitedBy', + createdAt: 'createdAt', + updatedAt: 'updatedAt', + }, subscription: { id: 'id', plan: 'plan', diff --git a/scripts/check-api-validation-contracts.ts b/scripts/check-api-validation-contracts.ts index 8289bba83d8..b6f582a143f 100644 --- a/scripts/check-api-validation-contracts.ts +++ b/scripts/check-api-validation-contracts.ts @@ -9,8 +9,8 @@ const QUERY_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/queries') const SELECTOR_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/selectors') const BASELINE = { - totalRoutes: 969, - zodRoutes: 969, + totalRoutes: 970, + zodRoutes: 970, nonZodRoutes: 0, } as const diff --git a/scripts/check-migrations-safety.ts b/scripts/check-migrations-safety.ts index 5c4e299af23..5008b48e367 100644 --- a/scripts/check-migrations-safety.ts +++ b/scripts/check-migrations-safety.ts @@ -25,6 +25,7 @@ * bun run scripts/check-migrations-safety.ts --dir # a directory */ import { execFileSync } from 'node:child_process' +import { existsSync } from 'node:fs' import { readdir, readFile } from 'node:fs/promises' import path from 'node:path' @@ -414,7 +415,9 @@ function changedMigrationFiles(baseRef: string): string[] { if (inDir(p)) files.add(p) } } - return [...files] + // A migration deleted in the working tree (e.g. regenerated before commit) has + // no SQL left to lint — skip it rather than crash on the read. + return [...files].filter((f) => existsSync(path.join(ROOT, f))) } async function listSqlFiles(dir: string): Promise {