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
6 changes: 5 additions & 1 deletion apps/sim/app/api/knowledge/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@ export const GET = defineInternalJsonRoute({
parseOptions: {
validationErrorResponse: (error) => validationErrorResponse(error, 'Invalid query parameters'),
},
mapInput: ({ query }) => ({ workspaceId: query.workspaceId, scope: query.scope }),
mapInput: ({ query }) => ({
workspaceId: query.workspaceId,
scope: query.scope,
includeCounts: query.includeCounts,
}),
useCase: listInternalKnowledgeBases,
present: internalKnowledgePresenters.list,
})
Expand Down
23 changes: 14 additions & 9 deletions apps/sim/app/api/v1/knowledge/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ import {
performDeleteKnowledgeBase,
performUpdateKnowledgeBase,
} from '@/lib/knowledge/orchestration'
import { attachKnowledgeBaseConnectors } from '@/lib/knowledge/service'
import {
formatKnowledgeBase,
handleError,
resolveKnowledgeBase,
resolveV1KnowledgeReadAccess,
} from '@/app/api/v1/knowledge/utils'
import { authenticateRequest, v1ValidationErrorResponse } from '@/app/api/v1/middleware'

Expand All @@ -41,19 +43,18 @@ export const GET = withRouteHandler(async (request: NextRequest, context: Knowle
if (!parsed.success) return parsed.response

const { id } = parsed.data.params
const result = await resolveKnowledgeBase(
id,
parsed.data.query.workspaceId,
userId,
rateLimit,
'knowledge.use'
)
const { workspaceId } = parsed.data.query
const result = await resolveKnowledgeBase(id, workspaceId, userId, rateLimit, 'knowledge.use')
if (result instanceof NextResponse) return result

const knowledgeBase = await attachKnowledgeBaseConnectors(
result.kb,
await resolveV1KnowledgeReadAccess(userId, rateLimit, workspaceId)
)
return NextResponse.json({
success: true,
data: {
knowledgeBase: formatKnowledgeBase(result.kb),
knowledgeBase: formatKnowledgeBase(knowledgeBase),
},
})
} catch (error) {
Expand Down Expand Up @@ -102,10 +103,14 @@ export const PUT = withRouteHandler(async (request: NextRequest, context: Knowle
)
}

const knowledgeBase = await attachKnowledgeBaseConnectors(
outcome.knowledgeBase,
await resolveV1KnowledgeReadAccess(userId, rateLimit, workspaceId)
)
return NextResponse.json({
success: true,
data: {
knowledgeBase: formatKnowledgeBase(outcome.knowledgeBase),
knowledgeBase: formatKnowledgeBase(knowledgeBase),
message: 'Knowledge base updated successfully',
},
})
Expand Down
137 changes: 137 additions & 0 deletions apps/sim/app/api/v1/knowledge/route.integration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/**
* Knowledge-base document totals against real PostgreSQL: the public v1 list and detail count
* only the documents their caller can read, and the internal list reads no document at all
* unless the caller asks for totals.
*/
import type { Principal } from '@sim/auth/principal'
import { db } from '@sim/db'
import { document, organization, user, workspace } from '@sim/db/schema'
import { createMockRequest } from '@sim/testing'
import { generateId } from '@sim/utils/id'
import { eq, inArray } from 'drizzle-orm'
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'

const caller = vi.hoisted(() => ({ userId: '' }))

vi.mock('@/app/api/v1/middleware', async (importOriginal) => ({
...(await importOriginal<typeof import('@/app/api/v1/middleware')>()),
authenticateRequest: async () => ({
requestId: 'fixture-request',
userId: caller.userId,
rateLimit: {
allowed: true,
remaining: 1,
limit: 1,
resetAt: new Date(),
userId: caller.userId,
keyType: 'personal',
},
}),
}))

import {
createKnowledgeAclFixtureIds,
seedKnowledgeAclFixture,
} from '@/lib/knowledge/__integration__/seed-source-access-fixture'
import { listInternalKnowledgeBases } from '@/lib/knowledge/application/knowledge-bases'
import { GET as getKnowledgeBase } from '@/app/api/v1/knowledge/[id]/route'
import { GET as listKnowledgeBases } from '@/app/api/v1/knowledge/route'

const ids = createKnowledgeAclFixtureIds()
const reader: Principal = { kind: 'session', userId: ids.bobId, sessionId: 'fixture-reader' }

describe('knowledge-base document totals in PostgreSQL', () => {
beforeAll(async () => {
vi.stubGlobal('fetch', async () => {
throw new Error('Unexpected provider request in knowledge-base count tests')
})
caller.userId = ids.bobId
await seedKnowledgeAclFixture(ids, { connectorType: 'google_drive' })
await db.insert(document).values([
{
id: generateId(),
knowledgeBaseId: ids.knowledgeBaseId,
filename: 'Uploaded handbook',
fileUrl: 'https://fixture.test/uploaded',
fileSize: 10,
mimeType: 'text/plain',
tokenCount: 10,
processingStatus: 'completed',
},
{
id: generateId(),
knowledgeBaseId: ids.knowledgeBaseId,
filename: 'Private source document',
fileUrl: 'https://fixture.test/private',
fileSize: 20,
mimeType: 'text/plain',
tokenCount: 20,
processingStatus: 'completed',
connectorId: ids.connectorId,
externalId: 'private-fixture',
contentHash: 'fixture',
acl: [`u:${ids.aliceId}@fixture.test`],
aclVerifiedAt: new Date(),
},
])
})

afterAll(async () => {
await db.delete(workspace).where(eq(workspace.id, ids.workspaceId))
await db.delete(organization).where(eq(organization.id, ids.organizationId))
await db.delete(user).where(inArray(user.id, [ids.aliceId, ids.bobId]))
vi.unstubAllGlobals()
})

it('lists only the documents a v1 caller can read', async () => {
const response = await listKnowledgeBases(
createMockRequest(
'GET',
undefined,
{},
`http://localhost/api/v1/knowledge?workspaceId=${ids.workspaceId}`
),
{ params: Promise.resolve({}) }
)
expect(response.status).toBe(200)
const { data } = await response.json()
expect(data.knowledgeBases).toEqual([
expect.objectContaining({ id: ids.knowledgeBaseId, docCount: 1, tokenCount: 10 }),
])
})

it('details only the documents a v1 caller can read', async () => {
const response = await getKnowledgeBase(
createMockRequest(
'GET',
undefined,
{},
`http://localhost/api/v1/knowledge/${ids.knowledgeBaseId}?workspaceId=${ids.workspaceId}`
),
{ params: Promise.resolve({ id: ids.knowledgeBaseId }) }
)
expect(response.status).toBe(200)
const { data } = await response.json()
expect(data.knowledgeBase).toMatchObject({
id: ids.knowledgeBaseId,
docCount: 1,
tokenCount: 10,
})
})

it('omits totals from the internal list unless the caller asks for them', async () => {
const input = { workspaceId: ids.workspaceId, scope: 'active' } as const
const [plain] = (await listInternalKnowledgeBases.execute({ principal: reader, input }))
.knowledgeBases
expect(plain).not.toHaveProperty('docCount')
expect(plain).not.toHaveProperty('tokenCount')

const [counted] = (
await listInternalKnowledgeBases.execute({
principal: reader,
input: { ...input, includeCounts: true },
})
).knowledgeBases
expect(counted).toMatchObject({ docCount: 1, tokenCount: 10 })
})
})
14 changes: 10 additions & 4 deletions apps/sim/app/api/v1/knowledge/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ import {
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { performCreateKnowledgeBase } from '@/lib/knowledge/orchestration'
import { getWorkspaceKnowledgeBases } from '@/lib/knowledge/service'
import { formatKnowledgeBase, handleError } from '@/app/api/v1/knowledge/utils'
import {
formatKnowledgeBase,
handleError,
resolveV1KnowledgeReadAccess,
} from '@/app/api/v1/knowledge/utils'
import {
authenticateRequest,
v1ValidationErrorResponse,
Expand Down Expand Up @@ -48,9 +52,11 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
)
if (accessError) return accessError

/** Read only after `validateWorkspaceAccess` authorized this caller; same list the
* internal surface serves, from the same place. */
const { data: knowledgeBases } = await getWorkspaceKnowledgeBases(workspaceId)
/** Read only after `validateWorkspaceAccess` authorized this caller, and totalled as the
* caller reads, exactly as the v1 document routes list. */
const { data: knowledgeBases } = await getWorkspaceKnowledgeBases(workspaceId, 'active', {
countsFor: await resolveV1KnowledgeReadAccess(userId, rateLimit, workspaceId),
})

return NextResponse.json({
success: true,
Expand Down
7 changes: 4 additions & 3 deletions apps/sim/app/api/v1/knowledge/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import {
WORKSPACE_ACCESS_SCOPE,
} from '@/lib/knowledge/access/scope'
import type { KnowledgeAccessProvider, KnowledgeAccessScope } from '@/lib/knowledge/access/types'
import { getKnowledgeBaseById } from '@/lib/knowledge/service'
import type { ActiveKnowledgeBaseReference } from '@/lib/knowledge/knowledge-base-reference'
import { getActiveKnowledgeBaseReference } from '@/lib/knowledge/service'
import type { KnowledgeBaseWithCounts } from '@/lib/knowledge/types'
import {
type RateLimitResult,
Expand All @@ -32,7 +33,7 @@ export async function resolveKnowledgeBase(
rateLimit: RateLimitResult,
capability: V1RouteCapability,
level: 'read' | 'write' = 'read'
): Promise<{ kb: KnowledgeBaseWithCounts } | NextResponse> {
): Promise<{ kb: ActiveKnowledgeBaseReference } | NextResponse> {
const accessError = await validateWorkspaceAccess(
rateLimit,
userId,
Expand All @@ -42,7 +43,7 @@ export async function resolveKnowledgeBase(
)
if (accessError) return accessError

const kb = await getKnowledgeBaseById(id)
const kb = await getActiveKnowledgeBaseReference(id)
if (!kb) {
return NextResponse.json({ error: 'Knowledge base not found' }, { status: 404 })
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,11 @@ export function useKnowledgeUpload(options: UseKnowledgeUploadOptions = {}) {
})
}

/** Reconciles both caches an upload moves: the base's documents and the list's `docCount`. */
/** Reconciles both caches an upload moves: the base's documents and the counted `docCount`. */
const invalidateKnowledgeCaches = async (knowledgeBaseId: string) => {
await Promise.all([
queryClient.invalidateQueries({ queryKey: knowledgeKeys.detail(knowledgeBaseId) }),
queryClient.invalidateQueries({ queryKey: knowledgeKeys.lists() }),
queryClient.invalidateQueries({ queryKey: knowledgeKeys.countedLists() }),
])
}

Expand Down
5 changes: 4 additions & 1 deletion apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,10 @@ function KnowledgeContent() {
}
}, [permissionConfig.hideKnowledgeBaseTab, router, workspaceId])

const { knowledgeBases, isLoading, isPlaceholderData, error } = useKnowledgeBasesList(workspaceId)
const { knowledgeBases, isLoading, isPlaceholderData, error } = useKnowledgeBasesList(
workspaceId,
{ includeCounts: true }
)
const { data: members } = useWorkspaceMembersQuery(workspaceId)
/**
* Indexed once: `ownerCell` resolves a member per row, so passing the raw array makes the
Expand Down
12 changes: 6 additions & 6 deletions apps/sim/app/workspace/[workspaceId]/knowledge/prefetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ import { prefetchResourceListChrome } from '@/app/workspace/[workspaceId]/lib/pr
import { KNOWLEDGE_BASE_LIST_STALE_TIME, knowledgeKeys } from '@/hooks/queries/utils/knowledge-keys'

/**
* Prefetches the workspace's knowledge-bases list AND its knowledge-base folder tree — plus
* the pinned ids and members {@link prefetchResourceListChrome} covers — under
* the same query keys the client `useKnowledgeBasesQuery` / `useFolders` hooks use (scope
* `active`), so the list paints populated on first render.
* Prefetches the workspace's knowledge-bases list with its document totals AND its
* knowledge-base folder tree — plus the pinned ids and members {@link prefetchResourceListChrome}
* covers — under the same query keys the Knowledge page's `useKnowledgeBasesQuery` (scope
* `active`, counted) and `useFolders` hooks use, so the list paints populated on first render.
*
* Both are needed: a base row is only placed correctly relative to the folder rows it sits
* beside, so prefetching one without the other still flashes an ungrouped list — and a
Expand Down Expand Up @@ -45,12 +45,12 @@ export async function prefetchKnowledgeBases(

await Promise.all([
queryClient.prefetchQuery({
queryKey: knowledgeKeys.list(workspaceId, 'active'),
queryKey: knowledgeKeys.countedList(workspaceId, 'active'),
queryFn: async () => {
const principal = await internalSessionAuth.authenticate()
const result = await listInternalKnowledgeBases.execute({
principal,
input: { workspaceId, scope: 'active' },
input: { workspaceId, scope: 'active', includeCounts: true },
})
return listKnowledgeBasesContract.response.schema.parse(
internalKnowledgePresenters.list(result)
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/app/workspace/[workspaceId]/lib/prefetch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ describe('workspace list prefetches', () => {
await prefetchKnowledgeBases(client, WORKSPACE_ID, USER_ID)

expect(mockListInternalKnowledgeBases).not.toHaveBeenCalled()
expect(client.getQueryData(knowledgeKeys.list(WORKSPACE_ID, 'active'))).toBeUndefined()
expect(client.getQueryData(knowledgeKeys.countedList(WORKSPACE_ID, 'active'))).toBeUndefined()
})
})

Expand Down
4 changes: 2 additions & 2 deletions apps/sim/hooks/kb/use-knowledge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,8 @@ export function useKnowledgeBaseDocuments(
* Hook to fetch and manage knowledge bases list
* Uses React Query as single source of truth
*/
export function useKnowledgeBasesList(workspaceId?: string) {
const query = useKnowledgeBasesQuery(workspaceId)
export function useKnowledgeBasesList(workspaceId?: string, options?: { includeCounts?: boolean }) {
const query = useKnowledgeBasesQuery(workspaceId, options)

return {
knowledgeBases: query.data ?? [],
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/hooks/queries/kb/connectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ export function useUpdateConnector() {
queryKey: knowledgeKeys.detail(knowledgeBaseId),
exact: true,
})
queryClient.invalidateQueries({ queryKey: knowledgeKeys.lists() })
queryClient.invalidateQueries({ queryKey: knowledgeKeys.countedLists() })
queryClient.invalidateQueries({ queryKey: knowledgeKeys.searches() })
}
},
Expand Down
Loading
Loading