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
4 changes: 4 additions & 0 deletions .claude/rules/sim-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ packages/ # @sim/* — audit, auth, db, logger, realtime-protocol
- `apps/* → packages/*` only. Packages never import from `apps/*`.
- `apps/realtime` avoids Next.js, React, the block/tool registry, provider SDKs, and the executor; never add `@/lib/webhooks/providers/*`, `@/executor/*`, `@/blocks/*`, or `@/tools/*` imports to any package it consumes. CI enforces this via `scripts/check-monorepo-boundaries.ts` and `scripts/check-realtime-prune-graph.ts`.

## The `'use client'` server boundary

Every export of a `'use client'` module becomes a *client reference* on the server — server-evaluated code (RSC pages/layouts, `prefetch.ts`, route handlers, block definitions, triggers) can only *render* it as a component or pass it as a prop, never *call* it (doing so throws at runtime, e.g. `tableKeys.list is not a function`; `next build` does not catch it). Keep server-importable query primitives (key factories, fetchers, mappers, constants) in non-`'use client'` modules — see `.claude/rules/sim-queries.md`. Enforced by `scripts/check-client-boundary-imports.ts`.

## Feature Organization

Features live under `app/workspace/[workspaceId]/`:
Expand Down
11 changes: 11 additions & 0 deletions .claude/rules/sim-queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@ Never use inline query keys — always use the factory.

**Every identifier the `queryFn` forwards into the fetch MUST appear in the `queryKey`.** (Query-machinery identifiers — `signal`, `pageParam` — are exempt; they aren't fetch-scoping args.) If the fetch is scoped by `workspaceId`, `cursor`, `limit`, an org id, etc., those values must be part of the key — otherwise distinct fetch args share one cache entry (a cross-tenant / per-param cache collision). The lone exception is a globally-unique id used as the key while a second fetch arg is only an authz scope that cannot collide; annotate those with `// rq-lint-allow: <reason>`. Enforced by the `key-fetch-arg-drift` check in `scripts/check-react-query-patterns.ts`.

## Server-importable query primitives must NOT live in a `'use client'` module

Next.js rewrites **every** export of a `'use client'` module into a *client reference* in the server bundle. Server-evaluated code — RSC `page.tsx`/`layout.tsx`, `prefetch.ts`, route handlers, **block definitions**, triggers/workers — can only *render* such an export as a component or pass it as a prop; **calling** one throws at runtime (`Attempted to call X from the server but X is on the client` — for an object export it surfaces as `X.list is not a function`). `next build` does **not** catch this — only SSR/runtime does.

So any **query-key factory, standalone `requestJson` fetcher, mapper, or constant** that a server module imports must live in a **non-`'use client'`** module:

- key factories → `hooks/queries/utils/<entity>-keys.ts` (see `folder-keys.ts`, `table-keys.ts`, `credential-keys.ts`)
- standalone fetchers/mappers → `hooks/queries/utils/fetch-*.ts` / `*-list-query.ts` (see `fetch-workflow-envelope.ts`, `fetch-credential-set.ts`)

The `'use client'` hook module then imports these back for its hooks. **Never** define a server-imported factory/fetcher directly in a `'use client'` hooks file — it crashes SSR (this caused the tables-page crash). Enforced for prefetch/route/trigger/block files by `scripts/check-client-boundary-imports.ts` (`bun run check:client-boundary`, run in CI). Escape hatch for a genuinely browser-only path: `// client-boundary-allow: <reason>` on the line above the import.

## File Structure

```typescript
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/test-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,9 @@ jobs:
- name: React Query pattern audit
run: bun run check:react-query

- name: Client boundary import audit
run: bun run check:client-boundary

- name: Verify realtime prune graph
run: bun run check:realtime-prune

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,15 @@ 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,
useSavePersonalEnvironment,
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'

Expand Down
3 changes: 2 additions & 1 deletion apps/sim/blocks/blocks/credential.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
14 changes: 1 addition & 13 deletions apps/sim/hooks/queries/credential-sets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import {
createCredentialSetContract,
createCredentialSetInvitationContract,
deleteCredentialSetContract,
getCredentialSetContract,
leaveCredentialSetContract,
listCredentialSetInvitationDetailsContract,
listCredentialSetInvitationsContract,
Expand All @@ -29,6 +28,7 @@ import {
removeCredentialSetMemberContract,
resendCredentialSetInvitationContract,
} from '@/lib/api/contracts'
import { fetchCredentialSetById } from '@/hooks/queries/utils/fetch-credential-set'

export type {
CreateCredentialSetData,
Expand Down Expand Up @@ -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),
Expand Down
34 changes: 2 additions & 32 deletions apps/sim/hooks/queries/credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Comment thread
waleedlatif1 marked this conversation as resolved.

/**
* Key prefix for OAuth credential queries.
Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/hooks/queries/invitations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/hooks/queries/organization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
24 changes: 24 additions & 0 deletions apps/sim/hooks/queries/utils/credential-keys.ts
Original file line number Diff line number Diff line change
@@ -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,
}
21 changes: 21 additions & 0 deletions apps/sim/hooks/queries/utils/fetch-credential-set.ts
Original file line number Diff line number Diff line change
@@ -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
}
20 changes: 20 additions & 0 deletions apps/sim/hooks/queries/utils/fetch-workspace-credentials.ts
Original file line number Diff line number Diff line change
@@ -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 ?? []
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}))

Expand Down
2 changes: 1 addition & 1 deletion apps/sim/lib/workflows/comparison/resolve-values.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"check:realtime-prune": "bun run scripts/check-realtime-prune-graph.ts",
"check:zustand-v5": "bun run scripts/check-zustand-v5-selectors.ts",
"check:react-query": "bun run scripts/check-react-query-patterns.ts --check",
"check:client-boundary": "bun run scripts/check-client-boundary-imports.ts --check",
"check:utils": "bun run scripts/check-utils-enforcement.ts",
"check:migrations": "bun run scripts/check-migrations-safety.ts",
"mship-contracts:generate": "bun run scripts/sync-mothership-stream-contract.ts",
Expand Down
Loading
Loading