mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
fix(ssr): harden credential query-key factory + fetchers against the 'use client' stub bug (#5206)
* fix(ssr): move credential query-key factory + fetchers to non-client modules
Preventively closes the same 'use client' SSR client-reference-stub class that
crashed the tables page. Server-evaluated modules (the credential block def, the
workflow-comparison helpers) imported workspaceCredentialKeys /
fetchWorkspaceCredentialList / fetchCredentialSetById from 'use client' hook
modules, where they resolve to client-reference stubs on the server (a future
server call path would throw 'X is not a function').
Extract them into non-client hooks/queries/utils/{credential-keys,
fetch-workspace-credentials,fetch-credential-set}.ts (mirroring folder-keys.ts /
fetch-workflow-envelope.ts) and import from there. No behavior change — these
values were only ever called from browser paths.
* docs+ci: codify the 'use client' server-import rule + add check:client-boundary
Document the Next.js rule that server code can only render a 'use client'
export as a component, never call it (server imports resolve to client-reference
stubs that throw — the tables-page crash). Add the rule to
.claude/rules/sim-queries.md + a cross-ref in sim-architecture.md.
Add scripts/check-client-boundary-imports.ts (wired into CI as check:client-boundary)
that flags any value import from a 'use client' module in a server-evaluated,
non-JSX surface (prefetch / route handler / trigger / block definition), so this
class can't silently recur. Escape hatch: // client-boundary-allow: <reason>.
This commit is contained in:
+2
-5
@@ -17,11 +17,7 @@ import type { WorkspaceEnvironmentData } from '@/lib/environment/api'
|
||||
import { UnsavedChangesModal } from '@/app/workspace/[workspaceId]/components/credential-detail'
|
||||
import { SecretValueField } from '@/app/workspace/[workspaceId]/settings/components/secrets/components/secret-value-field'
|
||||
import { isValidEnvVarName } from '@/executor/constants'
|
||||
import {
|
||||
useWorkspaceCredentials,
|
||||
type WorkspaceCredential,
|
||||
workspaceCredentialKeys,
|
||||
} from '@/hooks/queries/credentials'
|
||||
import { useWorkspaceCredentials, type WorkspaceCredential } from '@/hooks/queries/credentials'
|
||||
import {
|
||||
usePersonalEnvironment,
|
||||
useRemoveWorkspaceEnvironment,
|
||||
@@ -29,6 +25,7 @@ import {
|
||||
useUpsertWorkspaceEnvironment,
|
||||
useWorkspaceEnvironment,
|
||||
} from '@/hooks/queries/environment'
|
||||
import { workspaceCredentialKeys } from '@/hooks/queries/utils/credential-keys'
|
||||
import { useWorkspacePermissionsQuery } from '@/hooks/queries/workspace'
|
||||
import { useSettingsDirtyStore } from '@/stores/settings/dirty/store'
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@ import { CredentialIcon } from '@/components/icons'
|
||||
import { getServiceConfigByProviderId } from '@/lib/oauth/utils'
|
||||
import { getQueryClient } from '@/app/_shell/providers/get-query-client'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { fetchWorkspaceCredentialList, workspaceCredentialKeys } from '@/hooks/queries/credentials'
|
||||
import { workspaceCredentialKeys } from '@/hooks/queries/utils/credential-keys'
|
||||
import { fetchWorkspaceCredentialList } from '@/hooks/queries/utils/fetch-workspace-credentials'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
|
||||
interface CredentialBlockOutput {
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
createCredentialSetContract,
|
||||
createCredentialSetInvitationContract,
|
||||
deleteCredentialSetContract,
|
||||
getCredentialSetContract,
|
||||
leaveCredentialSetContract,
|
||||
listCredentialSetInvitationDetailsContract,
|
||||
listCredentialSetInvitationsContract,
|
||||
@@ -29,6 +28,7 @@ import {
|
||||
removeCredentialSetMemberContract,
|
||||
resendCredentialSetInvitationContract,
|
||||
} from '@/lib/api/contracts'
|
||||
import { fetchCredentialSetById } from '@/hooks/queries/utils/fetch-credential-set'
|
||||
|
||||
export type {
|
||||
CreateCredentialSetData,
|
||||
@@ -76,18 +76,6 @@ export function useCredentialSets(organizationId?: string, enabled = true) {
|
||||
})
|
||||
}
|
||||
|
||||
export async function fetchCredentialSetById(
|
||||
id: string,
|
||||
signal?: AbortSignal
|
||||
): Promise<CredentialSet | null> {
|
||||
if (!id) return null
|
||||
const data = await requestJson(getCredentialSetContract, {
|
||||
params: { id },
|
||||
signal,
|
||||
})
|
||||
return data.credentialSet ?? null
|
||||
}
|
||||
|
||||
export function useCredentialSetDetail(id?: string, enabled = true) {
|
||||
return useQuery<CredentialSet | null>({
|
||||
queryKey: credentialSetKeys.detail(id),
|
||||
|
||||
@@ -20,6 +20,8 @@ import {
|
||||
type WorkspaceCredentialType,
|
||||
} from '@/lib/api/contracts'
|
||||
import { environmentKeys } from '@/hooks/queries/environment'
|
||||
import { workspaceCredentialKeys } from '@/hooks/queries/utils/credential-keys'
|
||||
import { fetchWorkspaceCredentialList } from '@/hooks/queries/utils/fetch-workspace-credentials'
|
||||
|
||||
/**
|
||||
* Key prefix for OAuth credential queries.
|
||||
@@ -34,38 +36,6 @@ export type {
|
||||
WorkspaceCredentialType,
|
||||
}
|
||||
|
||||
export const workspaceCredentialKeys = {
|
||||
all: ['workspaceCredentials'] as const,
|
||||
lists: () => [...workspaceCredentialKeys.all, 'list'] as const,
|
||||
list: (workspaceId?: string, type?: string, providerId?: string) =>
|
||||
[
|
||||
...workspaceCredentialKeys.lists(),
|
||||
workspaceId ?? 'none',
|
||||
type ?? 'all',
|
||||
providerId ?? 'all',
|
||||
] as const,
|
||||
details: () => [...workspaceCredentialKeys.all, 'detail'] as const,
|
||||
detail: (credentialId?: string) =>
|
||||
[...workspaceCredentialKeys.details(), credentialId ?? 'none'] as const,
|
||||
members: (credentialId?: string) =>
|
||||
[...workspaceCredentialKeys.detail(credentialId), 'members'] as const,
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch workspace credential list from API.
|
||||
* Used by the prefetch function for hover-based cache warming.
|
||||
*/
|
||||
export async function fetchWorkspaceCredentialList(
|
||||
workspaceId: string,
|
||||
signal?: AbortSignal
|
||||
): Promise<WorkspaceCredential[]> {
|
||||
const data = await requestJson(listWorkspaceCredentialsContract, {
|
||||
query: { workspaceId },
|
||||
signal,
|
||||
})
|
||||
return data.credentials ?? []
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetch workspace credentials into a QueryClient cache.
|
||||
* Use on hover to warm data before navigation.
|
||||
|
||||
@@ -11,8 +11,8 @@ import {
|
||||
resendInvitationContract,
|
||||
} from '@/lib/api/contracts/invitations'
|
||||
import { updateWorkspacePermissionsContract } from '@/lib/api/contracts/workspaces'
|
||||
import { workspaceCredentialKeys } from '@/hooks/queries/credentials'
|
||||
import { organizationKeys } from '@/hooks/queries/organization'
|
||||
import { workspaceCredentialKeys } from '@/hooks/queries/utils/credential-keys'
|
||||
import { workspaceKeys } from '@/hooks/queries/workspace'
|
||||
|
||||
export const invitationKeys = {
|
||||
|
||||
@@ -42,8 +42,8 @@ import {
|
||||
import { client } from '@/lib/auth/auth-client'
|
||||
import { isEnterprise, isPaid, isTeam } from '@/lib/billing/plan-helpers'
|
||||
import { hasPaidSubscriptionStatus } from '@/lib/billing/subscriptions/utils'
|
||||
import { workspaceCredentialKeys } from '@/hooks/queries/credentials'
|
||||
import { subscriptionKeys } from '@/hooks/queries/subscription'
|
||||
import { workspaceCredentialKeys } from '@/hooks/queries/utils/credential-keys'
|
||||
import { workspaceKeys } from '@/hooks/queries/workspace'
|
||||
|
||||
const logger = createLogger('OrganizationQueries')
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* React Query key factory for workspace credentials.
|
||||
*
|
||||
* Lives in this standalone (non-`'use client'`) module — like
|
||||
* {@link file://./folder-keys.ts} — so server-evaluated code (block
|
||||
* definitions, server prefetch) can import it without pulling client-reference
|
||||
* stubs from the `'use client'` `@/hooks/queries/credentials` module.
|
||||
*/
|
||||
export const workspaceCredentialKeys = {
|
||||
all: ['workspaceCredentials'] as const,
|
||||
lists: () => [...workspaceCredentialKeys.all, 'list'] as const,
|
||||
list: (workspaceId?: string, type?: string, providerId?: string) =>
|
||||
[
|
||||
...workspaceCredentialKeys.lists(),
|
||||
workspaceId ?? 'none',
|
||||
type ?? 'all',
|
||||
providerId ?? 'all',
|
||||
] as const,
|
||||
details: () => [...workspaceCredentialKeys.all, 'detail'] as const,
|
||||
detail: (credentialId?: string) =>
|
||||
[...workspaceCredentialKeys.details(), credentialId ?? 'none'] as const,
|
||||
members: (credentialId?: string) =>
|
||||
[...workspaceCredentialKeys.detail(credentialId), 'members'] as const,
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { requestJson } from '@/lib/api/client/request'
|
||||
import { type CredentialSet, getCredentialSetContract } from '@/lib/api/contracts'
|
||||
|
||||
/**
|
||||
* Fetches a credential set by id (returns `null` for an empty id).
|
||||
*
|
||||
* Lives in this standalone (non-`'use client'`) module so server-reachable
|
||||
* workflow-comparison helpers can import it without pulling client-reference
|
||||
* stubs from the `'use client'` `@/hooks/queries/credential-sets` module.
|
||||
*/
|
||||
export async function fetchCredentialSetById(
|
||||
id: string,
|
||||
signal?: AbortSignal
|
||||
): Promise<CredentialSet | null> {
|
||||
if (!id) return null
|
||||
const data = await requestJson(getCredentialSetContract, {
|
||||
params: { id },
|
||||
signal,
|
||||
})
|
||||
return data.credentialSet ?? null
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { requestJson } from '@/lib/api/client/request'
|
||||
import { listWorkspaceCredentialsContract, type WorkspaceCredential } from '@/lib/api/contracts'
|
||||
|
||||
/**
|
||||
* Fetches the workspace credential list.
|
||||
*
|
||||
* Lives in this standalone (non-`'use client'`) module so block definitions and
|
||||
* server prefetch can import it without pulling client-reference stubs from the
|
||||
* `'use client'` `@/hooks/queries/credentials` module.
|
||||
*/
|
||||
export async function fetchWorkspaceCredentialList(
|
||||
workspaceId: string,
|
||||
signal?: AbortSignal
|
||||
): Promise<WorkspaceCredential[]> {
|
||||
const data = await requestJson(listWorkspaceCredentialsContract, {
|
||||
query: { workspaceId },
|
||||
signal,
|
||||
})
|
||||
return data.credentials ?? []
|
||||
}
|
||||
@@ -36,7 +36,7 @@ vi.mock('@/lib/workflows/subblocks/context', () => ({
|
||||
buildSelectorContextFromBlock: vi.fn(() => ({})),
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/queries/credential-sets', () => ({
|
||||
vi.mock('@/hooks/queries/utils/fetch-credential-set', () => ({
|
||||
fetchCredentialSetById: vi.fn(),
|
||||
}))
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ import { buildSelectorContextFromBlock } from '@/lib/workflows/subblocks/context
|
||||
import { getBlock } from '@/blocks/registry'
|
||||
import { SELECTOR_TYPES_HYDRATION_REQUIRED, type SubBlockConfig } from '@/blocks/types'
|
||||
import { CREDENTIAL_SET, isUuid } from '@/executor/constants'
|
||||
import { fetchCredentialSetById } from '@/hooks/queries/credential-sets'
|
||||
import { fetchOAuthCredentialDetail } from '@/hooks/queries/oauth/oauth-credentials'
|
||||
import { fetchCredentialSetById } from '@/hooks/queries/utils/fetch-credential-set'
|
||||
import { getSelectorDefinition, loadAllSelectorOptions } from '@/hooks/selectors/registry'
|
||||
import { resolveSelectorForSubBlock } from '@/hooks/selectors/resolution'
|
||||
import type { SelectorContext, SelectorKey } from '@/hooks/selectors/types'
|
||||
|
||||
Reference in New Issue
Block a user