Skip to content

Commit fda3082

Browse files
committed
fix(selectors): key CloudWatch lists by search, and stop a caller erasing the credential gate
The CloudWatch log-group and log-stream selectors forwarded `search` into the request as `prefix` but left it out of the query key, so every keystroke resolved to the same fresh entry and no refetch fired. Server-side filtering was dead: a log group outside the first page could not be reached. An audit of all 69 selector definitions found these two and no others. useSelectorOptions resolved `args.enabled ?? definition.enabled(...)`, so a caller supplying its own gate replaced the definition's precondition rather than narrowing it. useSelectorDisplayName knows nothing about credentials, so a card holding a saved value with no credential context ran a query that could only reject. The two are now conjoined. The detail hooks keep the override deliberately — resolving one known id needs less context than listing, which their TSDoc already documents. The list-key fix has a test, proven to fail without it. The `enabled` change has none: loading use-selector-query pulls the selector registry and emcn CSS, which cannot resolve in a git worktree.
1 parent 93e040f commit fda3082

3 files changed

Lines changed: 45 additions & 3 deletions

File tree

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { describe, expect, it } from 'vitest'
5+
import { cloudwatchSelectors } from '@/hooks/selectors/providers/cloudwatch/selectors'
6+
import type { SelectorQueryArgs } from '@/hooks/selectors/types'
7+
8+
const AWS_CONTEXT: SelectorQueryArgs['context'] = {
9+
awsAccessKeyId: 'AKIA',
10+
awsSecretAccessKey: 'secret',
11+
awsRegion: 'us-east-1',
12+
logGroupName: '/aws/lambda/fn',
13+
}
14+
15+
describe('cloudwatch selector query keys', () => {
16+
it.each([['cloudwatch.logGroups' as const], ['cloudwatch.logStreams' as const]])(
17+
'%s scopes its key by search, which fetchList forwards as `prefix`',
18+
(key) => {
19+
const definition = cloudwatchSelectors[key]
20+
const base: SelectorQueryArgs = { key, context: AWS_CONTEXT }
21+
22+
const noSearch = definition.getQueryKey(base)
23+
const withSearch = definition.getQueryKey({ ...base, search: 'api' })
24+
const otherSearch = definition.getQueryKey({ ...base, search: 'worker' })
25+
26+
expect(withSearch).not.toEqual(noSearch)
27+
expect(withSearch).not.toEqual(otherSearch)
28+
}
29+
)
30+
})

‎apps/sim/hooks/selectors/providers/cloudwatch/selectors.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,12 @@ export const cloudwatchSelectors = {
2020
key: 'cloudwatch.logGroups',
2121
contracts: [selectorContracts.cloudwatchLogGroupsSelectorContract],
2222
staleTime: SELECTOR_STALE,
23-
getQueryKey: ({ context }: SelectorQueryArgs) => [
23+
getQueryKey: ({ context, search }: SelectorQueryArgs) => [
2424
'selectors',
2525
'cloudwatch.logGroups',
2626
context.awsAccessKeyId ?? 'none',
2727
context.awsRegion ?? 'none',
28+
search ?? '',
2829
],
2930
enabled: ({ context }) =>
3031
Boolean(context.awsAccessKeyId && context.awsSecretAccessKey && context.awsRegion),
@@ -51,12 +52,13 @@ export const cloudwatchSelectors = {
5152
key: 'cloudwatch.logStreams',
5253
contracts: [selectorContracts.cloudwatchLogStreamsSelectorContract],
5354
staleTime: SELECTOR_STALE,
54-
getQueryKey: ({ context }: SelectorQueryArgs) => [
55+
getQueryKey: ({ context, search }: SelectorQueryArgs) => [
5556
'selectors',
5657
'cloudwatch.logStreams',
5758
context.awsAccessKeyId ?? 'none',
5859
context.awsRegion ?? 'none',
5960
context.logGroupName ?? 'none',
61+
search ?? '',
6062
],
6163
enabled: ({ context }) =>
6264
Boolean(

‎apps/sim/hooks/selectors/use-selector-query.ts‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,17 @@ export function useSelectorOptions(
7070
context: args.context,
7171
search: args.search,
7272
}
73-
const isEnabled = args.enabled ?? (definition.enabled ? definition.enabled(queryArgs) : true)
73+
/**
74+
* `definition.enabled` mirrors the preconditions the definition's own `fetchList` /
75+
* `fetchPage` assert (`ensureCredential`, `ensureKnowledgeBase`, an early `return []`),
76+
* so it is a hard precondition for the *list*, not a default a caller may replace.
77+
* A caller's `enabled` narrows further — it never widens — otherwise a card that only
78+
* knows it has a selection runs a fetch that is guaranteed to reject and caches the
79+
* rejection. Unlike {@link useSelectorOptionDetail}, resolving nothing here is never
80+
* cheaper than the list's own preconditions, so there is no case for an override.
81+
*/
82+
const isEnabled =
83+
(args.enabled ?? true) && (definition.enabled ? definition.enabled(queryArgs) : true)
7484
const supportsPagination = Boolean(definition.fetchPage)
7585

7686
const flatQuery = useQuery<SelectorOption[]>({

0 commit comments

Comments
 (0)