Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 38 additions & 14 deletions apps/sim/app/api/knowledge/[id]/tag-definitions/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { SUPPORTED_FIELD_TYPES } from '@/lib/knowledge/constants'
import { createTagDefinition, getTagDefinitions } from '@/lib/knowledge/tags/service'
import { checkKnowledgeBaseAccess } from '@/app/api/knowledge/utils'
Expand All @@ -19,19 +19,32 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id:
try {
logger.info(`[${requestId}] Getting tag definitions for knowledge base ${knowledgeBaseId}`)

const session = await getSession()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
const auth = await checkHybridAuth(req, { requireWorkflowId: false })
if (!auth.success) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}

const accessCheck = await checkKnowledgeBaseAccess(knowledgeBaseId, session.user.id)
if (!accessCheck.hasAccess) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
// Only allow session and internal JWT auth (not API key)
if (auth.authType === 'api_key') {
return NextResponse.json(
{ error: 'API key auth not supported for this endpoint' },
{ status: 401 }
)
}

// For session auth, verify KB access. Internal JWT is trusted.
if (auth.authType === 'session' && auth.userId) {
const accessCheck = await checkKnowledgeBaseAccess(knowledgeBaseId, auth.userId)
if (!accessCheck.hasAccess) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}
}

const tagDefinitions = await getTagDefinitions(knowledgeBaseId)

logger.info(`[${requestId}] Retrieved ${tagDefinitions.length} tag definitions`)
logger.info(
`[${requestId}] Retrieved ${tagDefinitions.length} tag definitions (${auth.authType})`
)

return NextResponse.json({
success: true,
Expand All @@ -51,14 +64,25 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
try {
logger.info(`[${requestId}] Creating tag definition for knowledge base ${knowledgeBaseId}`)

const session = await getSession()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
const auth = await checkHybridAuth(req, { requireWorkflowId: false })
if (!auth.success) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}

const accessCheck = await checkKnowledgeBaseAccess(knowledgeBaseId, session.user.id)
if (!accessCheck.hasAccess) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
// Only allow session and internal JWT auth (not API key)
if (auth.authType === 'api_key') {
return NextResponse.json(
{ error: 'API key auth not supported for this endpoint' },
{ status: 401 }
)
}

// For session auth, verify KB access. Internal JWT is trusted.
if (auth.authType === 'session' && auth.userId) {
const accessCheck = await checkKnowledgeBaseAccess(knowledgeBaseId, auth.userId)
if (!accessCheck.hasAccess) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}
}

const body = await req.json()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ interface DocumentSelectorProps {
onDocumentSelect?: (documentId: string) => void
isPreview?: boolean
previewValue?: string | null
previewContextValues?: Record<string, unknown>
}

export function DocumentSelector({
Expand All @@ -24,9 +25,15 @@ export function DocumentSelector({
onDocumentSelect,
isPreview = false,
previewValue,
previewContextValues,
}: DocumentSelectorProps) {
const { finalDisabled } = useDependsOnGate(blockId, subBlock, { disabled, isPreview })
const [knowledgeBaseIdValue] = useSubBlockValue(blockId, 'knowledgeBaseId')
const { finalDisabled } = useDependsOnGate(blockId, subBlock, {
disabled,
isPreview,
previewContextValues,
})
const [knowledgeBaseIdFromStore] = useSubBlockValue(blockId, 'knowledgeBaseId')
const knowledgeBaseIdValue = previewContextValues?.knowledgeBaseId ?? knowledgeBaseIdFromStore
const normalizedKnowledgeBaseId =
typeof knowledgeBaseIdValue === 'string' && knowledgeBaseIdValue.trim().length > 0
? knowledgeBaseIdValue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ interface DocumentTagEntryProps {
disabled?: boolean
isPreview?: boolean
previewValue?: any
previewContextValues?: Record<string, unknown>
}

/**
Expand All @@ -56,6 +57,7 @@ export function DocumentTagEntry({
disabled = false,
isPreview = false,
previewValue,
previewContextValues,
}: DocumentTagEntryProps) {
const [storeValue, setStoreValue] = useSubBlockValue<string>(blockId, subBlock.id)
const accessiblePrefixes = useAccessibleReferencePrefixes(blockId)
Expand All @@ -74,8 +76,12 @@ export function DocumentTagEntry({
disabled,
})

const [knowledgeBaseIdValue] = useSubBlockValue(blockId, 'knowledgeBaseId')
const knowledgeBaseId = knowledgeBaseIdValue || null
const [knowledgeBaseIdFromStore] = useSubBlockValue(blockId, 'knowledgeBaseId')
const knowledgeBaseIdValue = previewContextValues?.knowledgeBaseId ?? knowledgeBaseIdFromStore
const knowledgeBaseId =
typeof knowledgeBaseIdValue === 'string' && knowledgeBaseIdValue.trim().length > 0
? knowledgeBaseIdValue
: null

const { tagDefinitions, isLoading } = useKnowledgeBaseTagDefinitions(knowledgeBaseId)
const emitTagSelection = useTagSelection(blockId, subBlock.id)
Expand Down Expand Up @@ -131,11 +137,16 @@ export function DocumentTagEntry({
}

/**
* Removes a tag by ID (prevents removing the last tag)
* Removes a tag by ID, or resets it if it's the last one
*/
const removeTag = (id: string) => {
if (isReadOnly || tags.length === 1) return
updateTags(tags.filter((t) => t.id !== id))
if (isReadOnly) return
if (tags.length === 1) {
// Reset the last tag instead of removing it
updateTags([createDefaultTag()])
} else {
updateTags(tags.filter((t) => t.id !== id))
}
}

/**
Expand Down Expand Up @@ -222,6 +233,7 @@ export function DocumentTagEntry({

/**
* Renders the tag header with name, badge, and action buttons
* Shows tag name only when collapsed (as summary), generic label when expanded
*/
const renderTagHeader = (tag: DocumentTag, index: number) => (
<div
Expand All @@ -230,9 +242,11 @@ export function DocumentTagEntry({
>
<div className='flex min-w-0 flex-1 items-center gap-[8px]'>
<span className='block truncate font-medium text-[14px] text-[var(--text-tertiary)]'>
{tag.tagName || `Tag ${index + 1}`}
{tag.collapsed ? tag.tagName || `Tag ${index + 1}` : `Tag ${index + 1}`}
</span>
{tag.tagName && <Badge size='sm'>{FIELD_TYPE_LABELS[tag.fieldType] || 'Text'}</Badge>}
{tag.collapsed && tag.tagName && (
<Badge size='sm'>{FIELD_TYPE_LABELS[tag.fieldType] || 'Text'}</Badge>
)}
</div>
<div className='flex items-center gap-[8px] pl-[8px]' onClick={(e) => e.stopPropagation()}>
<Button
Expand All @@ -247,7 +261,7 @@ export function DocumentTagEntry({
<Button
variant='ghost'
onClick={() => removeTag(tag.id)}
disabled={isReadOnly || tags.length === 1}
disabled={isReadOnly}
className='h-auto p-0 text-[var(--text-error)] hover:text-[var(--text-error)]'
>
<Trash className='h-[14px] w-[14px]' />
Expand Down Expand Up @@ -341,7 +355,7 @@ export function DocumentTagEntry({

const tagOptions: ComboboxOption[] = selectableTags.map((t) => ({
value: t.displayName,
label: `${t.displayName} (${FIELD_TYPE_LABELS[t.fieldType] || 'Text'})`,
label: t.displayName,
}))

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ interface KnowledgeTagFiltersProps {
disabled?: boolean
isPreview?: boolean
previewValue?: string | null
previewContextValues?: Record<string, unknown>
}

/**
Expand All @@ -60,14 +61,19 @@ export function KnowledgeTagFilters({
disabled = false,
isPreview = false,
previewValue,
previewContextValues,
}: KnowledgeTagFiltersProps) {
const [storeValue, setStoreValue] = useSubBlockValue<string | null>(blockId, subBlock.id)
const emitTagSelection = useTagSelection(blockId, subBlock.id)
const valueInputRefs = useRef<Record<string, HTMLInputElement>>({})
const overlayRefs = useRef<Record<string, HTMLDivElement>>({})

const [knowledgeBaseIdValue] = useSubBlockValue(blockId, 'knowledgeBaseId')
const knowledgeBaseId = knowledgeBaseIdValue || null
const [knowledgeBaseIdFromStore] = useSubBlockValue(blockId, 'knowledgeBaseId')
const knowledgeBaseIdValue = previewContextValues?.knowledgeBaseId ?? knowledgeBaseIdFromStore
const knowledgeBaseId =
typeof knowledgeBaseIdValue === 'string' && knowledgeBaseIdValue.trim().length > 0
? knowledgeBaseIdValue
: null

const { tagDefinitions, isLoading } = useKnowledgeBaseTagDefinitions(knowledgeBaseId)
const accessiblePrefixes = useAccessibleReferencePrefixes(blockId)
Expand Down Expand Up @@ -123,11 +129,16 @@ export function KnowledgeTagFilters({
}

/**
* Removes a filter by ID (prevents removing the last filter)
* Removes a filter by ID, or resets it if it's the last one
*/
const removeFilter = (id: string) => {
if (isReadOnly || filters.length === 1) return
updateFilters(filters.filter((f) => f.id !== id))
if (isReadOnly) return
if (filters.length === 1) {
// Reset the last filter instead of removing it
updateFilters([createDefaultFilter()])
} else {
updateFilters(filters.filter((f) => f.id !== id))
}
}

/**
Expand Down Expand Up @@ -215,6 +226,7 @@ export function KnowledgeTagFilters({

/**
* Renders the filter header with name, badge, and action buttons
* Shows tag name only when collapsed (as summary), generic label when expanded
*/
const renderFilterHeader = (filter: TagFilter, index: number) => (
<div
Expand All @@ -223,9 +235,11 @@ export function KnowledgeTagFilters({
>
<div className='flex min-w-0 flex-1 items-center gap-[8px]'>
<span className='block truncate font-medium text-[14px] text-[var(--text-tertiary)]'>
{filter.tagName || `Filter ${index + 1}`}
{filter.collapsed ? filter.tagName || `Filter ${index + 1}` : `Filter ${index + 1}`}
</span>
{filter.tagName && <Badge size='sm'>{FIELD_TYPE_LABELS[filter.fieldType] || 'Text'}</Badge>}
{filter.collapsed && filter.tagName && (
<Badge size='sm'>{FIELD_TYPE_LABELS[filter.fieldType] || 'Text'}</Badge>
)}
</div>
<div className='flex items-center gap-[8px] pl-[8px]' onClick={(e) => e.stopPropagation()}>
<Button variant='ghost' onClick={addFilter} disabled={isReadOnly} className='h-auto p-0'>
Expand All @@ -235,7 +249,7 @@ export function KnowledgeTagFilters({
<Button
variant='ghost'
onClick={() => removeFilter(filter.id)}
disabled={isReadOnly || filters.length === 1}
disabled={isReadOnly}
className='h-auto p-0 text-[var(--text-error)] hover:text-[var(--text-error)]'
>
<Trash className='h-[14px] w-[14px]' />
Expand Down Expand Up @@ -324,7 +338,7 @@ export function KnowledgeTagFilters({
const renderFilterContent = (filter: TagFilter) => {
const tagOptions: ComboboxOption[] = tagDefinitions.map((tag) => ({
value: tag.displayName,
label: `${tag.displayName} (${FIELD_TYPE_LABELS[tag.fieldType] || 'Text'})`,
label: tag.displayName,
}))

const operators = getOperatorsForFieldType(filter.fieldType)
Expand Down
Loading