Skip to content

Commit 399149c

Browse files
committed
refactor(knowledge): collapse the access-mode vocabulary and the duplication around it
/simplify and /cleanup over the branch. The mode literal 'admin' appeared in eight modules deciding four different things; it is now one leaf predicate each site reads. - access-modes owns MIRRORING_ACCESS_MODES/mirrorsSourceAcls and aclIsDerived; the identity mapper, the second name for the same union, and the duplicate credential-backed predicate are gone, and the contract derives its enum from the leaf - listing caps are stripped once per rule rather than at three depths: create, the mode switch, and a config edit all key off aclIsDerived, so the stored config agrees with what runs - the mirroring assertion moved into the source-config validator the application layer already owns, so orchestration stays mode-agnostic - one resolver for a connector's token user, shared by the engine and the directory refresh, replacing two copies that had already drifted - Drive asks for permissions only on a run the engine says mirrors, drains its permission pages through the shared Google helper, and spreads the ACL context instead of relisting its fields - Confluence memoises its three per-run lookups through one helper, keys pages by one map, drops the unreachable space-principal branch, and shares the cursor parser with the content listing - nested Drive subgroups are read once per directory rather than once per parent; the scheduler dispatches with bounded concurrency; batch loops use chunkArray Behaviour is unchanged except where the reviews found it wrong: a config edit on a mirroring connector now strips caps and re-asserts the administrator, and a crawl that is not mirroring no longer pulls a permission array per file.
1 parent 6f204a0 commit 399149c

32 files changed

Lines changed: 520 additions & 506 deletions

File tree

apps/sim/app/api/knowledge/connectors/directory-sync/route.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,20 @@ import { getErrorMessage } from '@sim/utils/errors'
55
import { and, asc, eq, inArray, isNotNull, isNull } from 'drizzle-orm'
66
import type { NextRequest } from 'next/server'
77
import { verifyCronAuth } from '@/lib/auth/internal'
8+
import { mapWithConcurrency } from '@/lib/core/utils/concurrency'
89
import { generateRequestId } from '@/lib/core/utils/request'
910
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
11+
import { MIRRORING_ACCESS_MODES } from '@/lib/knowledge/connectors/access-modes'
1012
import { dispatchDirectorySync } from '@/lib/knowledge/connectors/directory-queue'
1113
import { RUNNABLE_CONNECTOR_STATUSES } from '@/lib/knowledge/connectors/sync-lock'
1214

1315
export const dynamic = 'force-dynamic'
1416

1517
const logger = createLogger('ConnectorDirectorySyncSchedulerAPI')
1618

17-
/** Connectors offered per tick. */
19+
/** Connectors offered per tick, and how many dispatches are in flight at once. */
1820
const MAX_DIRECTORIES_PER_TICK = 200
21+
const DISPATCH_CONCURRENCY = 8
1922

2023
/**
2124
* Refreshes the external directories that admin-mode connectors mirror.
@@ -48,7 +51,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
4851
.innerJoin(knowledgeBase, eq(knowledgeConnector.knowledgeBaseId, knowledgeBase.id))
4952
.where(
5053
and(
51-
eq(knowledgeConnector.accessMode, 'admin'),
54+
inArray(knowledgeConnector.accessMode, MIRRORING_ACCESS_MODES),
5255
inArray(knowledgeConnector.status, RUNNABLE_CONNECTOR_STATUSES),
5356
isNull(knowledgeConnector.archivedAt),
5457
isNull(knowledgeConnector.deletedAt),
@@ -61,7 +64,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
6164

6265
let dispatched = 0
6366
let failed = 0
64-
for (const { id: connectorId } of connectors) {
67+
await mapWithConcurrency(connectors, DISPATCH_CONCURRENCY, async ({ id: connectorId }) => {
6568
try {
6669
await dispatchDirectorySync(connectorId, { requestId, tickAt })
6770
dispatched += 1
@@ -72,7 +75,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
7275
error: getErrorMessage(error),
7376
})
7477
}
75-
}
78+
})
7679

7780
const summary = { considered: connectors.length, dispatched, failed }
7881
logger.info(`[${requestId}] Connector directory sync scheduler finished`, summary)

apps/sim/app/api/knowledge/connectors/sync/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
306306
.where(
307307
and(
308308
inArray(knowledgeConnector.status, RUNNABLE_CONNECTOR_STATUSES),
309-
inArray(knowledgeConnector.accessMode, [...CONTENT_ENGINE_ACCESS_MODES]),
309+
inArray(knowledgeConnector.accessMode, CONTENT_ENGINE_ACCESS_MODES),
310310
lte(knowledgeConnector.nextSyncAt, now),
311311
isNull(knowledgeConnector.archivedAt),
312312
isNull(knowledgeConnector.deletedAt),

apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ import {
4343
import { MaxBadge } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/max-badge'
4444
import { useConnectorConfigFields } from '@/app/workspace/[workspaceId]/knowledge/[id]/hooks/use-connector-config-fields'
4545
import {
46-
memberCapFieldIds,
46+
derivedAclCapFieldIds,
4747
useConnectorMemberGroupOptions,
4848
} from '@/app/workspace/[workspaceId]/knowledge/[id]/hooks/use-connector-member-group-options'
4949
import { useWorkspaceHostContext } from '@/app/workspace/[workspaceId]/providers/workspace-host-provider'
@@ -112,7 +112,7 @@ export function AddConnectorModal({
112112
const membersChoiceOpen =
113113
isMembersMode && groupOptions.needsChoice && !access.credentialGroupOptionId
114114
const hiddenCapFieldIds = useMemo(
115-
() => memberCapFieldIds(connectorConfig, access.accessMode),
115+
() => derivedAclCapFieldIds(connectorConfig, access.accessMode),
116116
[connectorConfig, access.accessMode]
117117
)
118118
/** True when the connector declares its key optional (public sources need none). */

apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-access-field/connector-access-field.tsx

Lines changed: 28 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -42,21 +42,20 @@ interface ConnectorAccessFieldProps {
4242

4343
/**
4444
* Each mode decides who can read the indexed documents, which the three labels
45-
* cannot say on their own — and getting it wrong is the kind of mistake that is
46-
* only noticed once the wrong person finds a document.
45+
* cannot say on their own.
4746
*/
48-
function accessHint(input: {
49-
mode: ConnectorAccessMode
50-
connectorConfig: ConnectorMeta
47+
function accessHint(
48+
mode: ConnectorAccessMode,
49+
sourceName: string,
5150
allowMembers: boolean
52-
}): string | undefined {
53-
if (input.mode === 'members') {
54-
return `Everyone in the workspace is invited by email to connect their ${input.connectorConfig.name} account when the first sync starts. Each member sees only the documents their own account can open; scheduled, API, and chat runs see workspace-visible documents only.`
51+
): string | undefined {
52+
if (mode === 'members') {
53+
return `Everyone in the workspace is invited by email to connect their ${sourceName} account when the first sync starts. Each member sees only the documents their own account can open; scheduled, API, and chat runs see workspace-visible documents only.`
5554
}
56-
if (input.mode === 'admin') {
57-
return `Indexed once as an administrator, keeping each document's own permissions. People see only what ${input.connectorConfig.name} already lets them open; scheduled, API, and chat runs see workspace-visible documents only.`
55+
if (mode === 'admin') {
56+
return `Indexed once as an administrator, keeping each document's own permissions. People see only what ${sourceName} already lets them open; scheduled, API, and chat runs see workspace-visible documents only.`
5857
}
59-
return input.allowMembers ? undefined : 'Per-member access is turned off for this workspace.'
58+
return allowMembers ? undefined : 'Per-member access is turned off for this workspace.'
6059
}
6160

6261
/**
@@ -90,25 +89,26 @@ export function ConnectorAccessField({
9089
const showAdmin = allowAdmin && Boolean(connectorConfig.mirrorsSourceAcls)
9190
if (!membersSupported && !showAdmin) return null
9291

92+
/** One ordered list, rendered by both the read-only and the editable branch. */
93+
const modes: { mode: ConnectorAccessMode; label: string; shown: boolean }[] = [
94+
{ mode: 'workspace', label: 'Workspace', shown: true },
95+
{ mode: 'members', label: 'Per member', shown: membersSupported },
96+
{ mode: 'admin', label: 'Mirror source', shown: showAdmin },
97+
]
98+
const modeItems = (isDisabled: (mode: ConnectorAccessMode) => boolean) =>
99+
modes
100+
.filter((entry) => entry.shown)
101+
.map((entry) => (
102+
<ButtonGroupItem key={entry.mode} value={entry.mode} disabled={isDisabled(entry.mode)}>
103+
{entry.label}
104+
</ButtonGroupItem>
105+
))
106+
93107
if (!canAdmin) {
94108
if (value.accessMode === 'workspace') return null
95109
return (
96110
<ChipModalField type='custom' title='Access'>
97-
<ButtonGroup value={value.accessMode}>
98-
<ButtonGroupItem value='workspace' disabled>
99-
Workspace
100-
</ButtonGroupItem>
101-
{membersSupported ? (
102-
<ButtonGroupItem value='members' disabled>
103-
Per member
104-
</ButtonGroupItem>
105-
) : null}
106-
{showAdmin ? (
107-
<ButtonGroupItem value='admin' disabled>
108-
Mirror source
109-
</ButtonGroupItem>
110-
) : null}
111-
</ButtonGroup>
111+
<ButtonGroup value={value.accessMode}>{modeItems(() => true)}</ButtonGroup>
112112
</ChipModalField>
113113
)
114114
}
@@ -125,7 +125,7 @@ export function ConnectorAccessField({
125125
type='custom'
126126
title='Access'
127127
error={error?.message}
128-
hint={accessHint({ mode: value.accessMode, connectorConfig, allowMembers })}
128+
hint={accessHint(value.accessMode, connectorConfig.name, allowMembers)}
129129
>
130130
<div className='flex flex-col gap-2'>
131131
<ButtonGroup
@@ -134,19 +134,7 @@ export function ConnectorAccessField({
134134
if (isConnectorAccessMode(mode)) onChange({ accessMode: mode })
135135
}}
136136
>
137-
<ButtonGroupItem value='workspace' disabled={disabled}>
138-
Workspace
139-
</ButtonGroupItem>
140-
{membersSupported ? (
141-
<ButtonGroupItem value='members' disabled={disabled || !allowMembers}>
142-
Per member
143-
</ButtonGroupItem>
144-
) : null}
145-
{showAdmin ? (
146-
<ButtonGroupItem value='admin' disabled={disabled}>
147-
Mirror source
148-
</ButtonGroupItem>
149-
) : null}
137+
{modeItems((mode) => disabled || (mode === 'members' && !allowMembers))}
150138
</ButtonGroup>
151139

152140
{value.accessMode === 'members' && showPicker && (

apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/edit-connector-modal.tsx

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { RefreshCw, SquareArrowUpRight } from '@sim/emcn/icons'
2121
import { createLogger } from '@sim/logger'
2222
import { useParams } from 'next/navigation'
2323
import type { ConnectorAccessMode } from '@/lib/api/contracts/knowledge/connectors'
24-
import { isCredentialBackedAccessMode } from '@/lib/knowledge/connectors/access-modes'
24+
import { isContentEngineAccessMode } from '@/lib/knowledge/connectors/access-modes'
2525
import { getProviderIdFromServiceId, type OAuthProvider } from '@/lib/oauth'
2626
import {
2727
ConnectorAccessField,
@@ -40,7 +40,7 @@ import type {
4040
} from '@/app/workspace/[workspaceId]/knowledge/[id]/hooks/use-connector-config-fields'
4141
import { useConnectorConfigFields } from '@/app/workspace/[workspaceId]/knowledge/[id]/hooks/use-connector-config-fields'
4242
import {
43-
memberCapFieldIds,
43+
derivedAclCapFieldIds,
4444
useConnectorMemberGroupOptions,
4545
} from '@/app/workspace/[workspaceId]/knowledge/[id]/hooks/use-connector-member-group-options'
4646
import { useWorkspaceHostContext } from '@/app/workspace/[workspaceId]/providers/workspace-host-provider'
@@ -60,6 +60,21 @@ import { useOAuthCredentials } from '@/hooks/queries/oauth/oauth-credentials'
6060

6161
const logger = createLogger('EditConnectorModal')
6262

63+
/** What the apply button says, and what it warns will happen, per mode. */
64+
const SWITCH_LABEL: Record<ConnectorAccessMode, string> = {
65+
workspace: 'Switch to workspace access',
66+
members: 'Switch to per-member access',
67+
admin: 'Switch to mirrored access',
68+
}
69+
70+
const SWITCH_NOTICE: Record<ConnectorAccessMode, string> = {
71+
workspace: 'Every workspace member can read every synced document once the next sync completes.',
72+
members:
73+
'Everyone in the workspace is invited to connect their account. Documents stay hidden until members connect and sync; listing caps are cleared.',
74+
admin:
75+
'Documents stay hidden until the next sync mirrors their permissions from the source; listing caps are cleared.',
76+
}
77+
6378
/** Keys injected by the sync engine or modal state — not user-editable */
6479
const INTERNAL_CONFIG_KEYS = new Set(['tagSlotMapping', 'disabledTagIds', '_canonicalModes'])
6580

@@ -247,9 +262,10 @@ export function EditConnectorModal({
247262
const { mutate: updateAccess, isPending: isSwitchingAccess } = useUpdateConnectorAccess()
248263
const isSaving = isSavingSettings || isSwitchingAccess
249264
/**
250-
* The field shows where the flag is on. A connector already syncing per
251-
* member keeps it where the flag has since been turned off, so an admin can
252-
* still bring it back to workspace mode; per-member cannot be re-chosen.
265+
* The field shows where either flag is on. A connector already in a
266+
* non-workspace mode keeps it where its flag has since been turned off, so
267+
* an admin can still bring it back to workspace mode; that mode cannot be
268+
* re-chosen.
253269
*/
254270
const memberAccessAvailable = features?.knowledgeMemberAccess === true
255271
const mirroredAccessAvailable = features?.knowledgeSourceMirroredAccess === true
@@ -268,7 +284,7 @@ export function EditConnectorModal({
268284
/** Leaving members mode for a mode that syncs with one credential needs that credential. */
269285
const needsWorkspaceCredential =
270286
accessDirty &&
271-
isCredentialBackedAccessMode(access.accessMode) &&
287+
isContentEngineAccessMode(access.accessMode) &&
272288
persistedAccess.accessMode === 'members'
273289
const accessComplete =
274290
!accessDirty ||
@@ -278,7 +294,7 @@ export function EditConnectorModal({
278294
/** A disabled member sync is re-enabled by applying the current binding again. */
279295
const canReenableMemberSync =
280296
!accessDirty && connector.accessMode === 'members' && connector.memberSyncStatus === 'disabled'
281-
const hiddenCapFieldIds = memberCapFieldIds(connectorConfig, access.accessMode)
297+
const hiddenCapFieldIds = derivedAclCapFieldIds(connectorConfig, access.accessMode)
282298

283299
const persistedCanonicalModes = useMemo(
284300
() => readPersistedCanonicalModes(connector.sourceConfig),
@@ -606,11 +622,7 @@ function SettingsTab({
606622
? 'Switching…'
607623
: isRebind
608624
? 'Change credential group'
609-
: access.accessMode === 'members'
610-
? 'Switch to per-member access'
611-
: access.accessMode === 'admin'
612-
? 'Switch to mirrored access'
613-
: 'Switch to workspace access'}
625+
: SWITCH_LABEL[access.accessMode]}
614626
</Button>
615627
<Button variant='default' size='sm' onClick={onResetAccess} disabled={isSaving}>
616628
Cancel
@@ -619,11 +631,7 @@ function SettingsTab({
619631
<p className='text-[var(--text-muted)] text-caption leading-snug'>
620632
{isRebind
621633
? 'Members of the previous group lose access; members of the new group are invited to connect.'
622-
: access.accessMode === 'members'
623-
? 'Everyone in the workspace is invited to connect their account. Documents stay hidden until members connect and sync; listing caps are cleared.'
624-
: access.accessMode === 'admin'
625-
? 'Documents stay hidden until the next sync mirrors their permissions from the source; listing caps are cleared.'
626-
: 'Every workspace member can read every synced document once the next sync completes.'}
634+
: SWITCH_NOTICE[access.accessMode]}
627635
</p>
628636
</div>
629637
) : undefined

apps/sim/app/workspace/[workspaceId]/knowledge/[id]/hooks/use-connector-member-group-options.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
getCredentialGroupProviderId,
1010
isCredentialGroupProvider,
1111
} from '@/lib/credential-groups/providers'
12+
import { aclIsDerived } from '@/lib/knowledge/connectors/access-modes'
1213
import type { ConnectorMeta } from '@/connectors/types'
1314
import { useCredentialGroups } from '@/hooks/queries/credential-groups'
1415

@@ -40,12 +41,12 @@ export function connectorMemberGroupProvider(
4041
}
4142

4243
/** The config fields a per-member connector hides: its listing caps, which the server clears. */
43-
export function memberCapFieldIds(
44+
export function derivedAclCapFieldIds(
4445
connectorConfig: ConnectorMeta | null,
4546
accessMode: ConnectorAccessMode
4647
): ReadonlySet<string> {
4748
return new Set(
48-
accessMode === 'members' ? (connectorConfig?.permissionScopedListing?.capFieldIds ?? []) : []
49+
aclIsDerived(accessMode) ? (connectorConfig?.permissionScopedListing?.capFieldIds ?? []) : []
4950
)
5051
}
5152

apps/sim/connectors/confluence/confluence.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ import {
1010
buildLastModifiedClause,
1111
confluenceConnector,
1212
escapeCql,
13-
extractCursor,
1413
isCurrentContent,
1514
preserveConfluenceCallouts,
1615
readIncludedLabels,
1716
} from '@/connectors/confluence/confluence'
17+
import { extractCursor } from '@/connectors/confluence/cursor'
1818
import { htmlToPlainText } from '@/connectors/utils'
1919

2020
describe('escapeCql', () => {

0 commit comments

Comments
 (0)