Skip to content
15 changes: 9 additions & 6 deletions apps/sim/lib/billing/core/billing-attribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -742,13 +742,16 @@ export async function resolveBillingAttribution({

/** The organization payer is independent of the person making the request. */
export async function resolveOrganizationBillingPayer(organizationId: string) {
const [owner] = await db
.select({ userId: member.userId })
.from(member)
.where(and(eq(member.organizationId, organizationId), eq(member.role, 'owner')))
.limit(1)
/** The owner and the subscription are independent reads; neither waits on the other. */
const [[owner], payerSubscription] = await Promise.all([
db
.select({ userId: member.userId })
.from(member)
.where(and(eq(member.organizationId, organizationId), eq(member.role, 'owner')))
.limit(1),
getOrganizationSubscription(organizationId, { onError: 'throw' }),
])
if (!owner) throw new Error('Organization billing owner is unavailable')
const payerSubscription = await getOrganizationSubscription(organizationId, { onError: 'throw' })
if (payerSubscription && payerSubscription.referenceId !== organizationId)
throw new Error('Organization subscription belongs to a different payer')
return { organizationId, billedAccountUserId: owner.userId, payerSubscription }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from '@/lib/knowledge/__integration__/seed-source-access-fixture'
import { createKnowledgeAccessProvider } from '@/lib/knowledge/access/scope'
import {
forgetProjectionFilled,
resolvePermittedDocuments,
retrieveKnowledgeSearch,
VECTOR_PROBE_DOCUMENT_LIMIT,
Expand Down Expand Up @@ -92,6 +93,8 @@ describe('API-key KB block fan-out', () => {
it.each([false, true])(
'completes 18 concurrent KB searches with access checks intact (tag filter: %s)',
async (withTags) => {
/** The projection-fill memo outlives an iteration; each one must read it once, like a cold process. */
forgetProjectionFilled()
const previousDebug = db.$client.options.debug
const statements: string[] = []
db.$client.options.debug = (_connection, query) => {
Expand Down Expand Up @@ -132,18 +135,20 @@ describe('API-key KB block fan-out', () => {
statements.filter((query) => query.includes(fragment))
/**
* Every statement runs under the leg's deadline: the candidate search applies it with the
* scan settings in one statement, and the probe, the exact ranking, the rerank and
* hydration each open with one of their own.
* scan settings in one statement, and the probe, the exact ranking and hydration each
* open with one of their own. The projection-fill read is shared by the searches that
* miss its memo together, so it appears once.
*/
expect(matching('statement_timeout')).toHaveLength(bases.length * 5)
expect(matching('statement_timeout')).toHaveLength(bases.length * 4 + 1)
Comment thread
waleedlatif1 marked this conversation as resolved.
/**
* A scope this small leaves the bounded traversal short of its candidate limit, so every
* search probes once and rescues once — never a widening retry loop.
*/
expect(matching('hnsw.iterative_scan')).toHaveLength(bases.length)
expect(matching('AS visible')).toHaveLength(bases.length)
expect(matching(') + 0 LIMIT')).toHaveLength(bases.length)
expect(matching('"embedding_search"."id" = ANY(')).toHaveLength(bases.length)
/** The walk carries each candidate's identities, so a filled projection reads no page. */
expect(matching('"embedding_search"."id" = ANY(')).toHaveLength(0)
/** The probe enumerates visible documents and reports saturation; it never ranks them. */
expect(
statements.filter(
Expand Down
15 changes: 9 additions & 6 deletions apps/sim/lib/knowledge/access/availability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,10 @@ export async function resolveKnowledgeAccessAvailability(
throw new Error('Knowledge access requires one resource owner')
/** A caller that brings its own billing snapshot is answered from that snapshot, uncached. */
if (context.ownerBilling) return readKnowledgeAccessAvailability(context)
const key = `${context.organizationId ?? ''}|${context.workspaceId ?? ''}|${context.userId ?? ''}`
/** An organization's answer depends on the organization alone; a workspace's on its viewer too. */
const key = context.organizationId
? `${context.organizationId}||`
: `|${context.workspaceId ?? ''}|${context.userId ?? ''}`
const availability = await availabilityCache.fetch(key, { context })
if (!availability) throw new Error('Knowledge access availability could not be resolved')
return availability
Expand All @@ -89,14 +92,14 @@ async function readKnowledgeAccessAvailability(
return { sourceMirrored: false, memberScoped: false }
}
if (context.organizationId) {
return {
sourceMirrored:
!isHosted || (await isOrganizationOnEnterprisePlan(context.organizationId, 'throw')),
memberScoped: await isScopedCredentialGroupsAvailable({
const [enterprise, memberScoped] = await Promise.all([
isHosted ? isOrganizationOnEnterprisePlan(context.organizationId, 'throw') : true,
isScopedCredentialGroupsAvailable({
kind: 'organization',
organizationId: context.organizationId,
}),
}
])
return { sourceMirrored: enterprise, memberScoped }
}
if (!context.workspaceId) throw new Error('Knowledge access requires a resource owner')
const ownerBilling =
Expand Down
7 changes: 2 additions & 5 deletions apps/sim/lib/knowledge/application/contexts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,12 @@ import {
} from '@/lib/knowledge/connectors/service'
import type { ActiveKnowledgeDocument } from '@/lib/knowledge/documents/service'
import { getKnowledgeDocument, getKnowledgeDocumentById } from '@/lib/knowledge/documents/service'
import type { ActiveKnowledgeBaseReference } from '@/lib/knowledge/knowledge-base-reference'
import {
getRestorableKnowledgeBase,
type RestorableKnowledgeBase,
} from '@/lib/knowledge/orchestration/restore'
import {
type ActiveKnowledgeBaseReference,
getActiveKnowledgeBaseReference,
getKnowledgeBaseById,
} from '@/lib/knowledge/service'
import { getActiveKnowledgeBaseReference, getKnowledgeBaseById } from '@/lib/knowledge/service'
import { getTagDefinitionById } from '@/lib/knowledge/tags/service'
import type { DocumentTagDefinition } from '@/lib/knowledge/tags/types'
import type { KnowledgeBaseWithCounts } from '@/lib/knowledge/types'
Expand Down
Loading
Loading