Skip to content

Commit eb78eec

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
refactor(netsuite): align integration with codebase patterns
1 parent 69d5519 commit eb78eec

16 files changed

Lines changed: 109 additions & 978 deletions

File tree

apps/sim/app/api/credentials/route.test.ts

Lines changed: 1 addition & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,10 @@ import { TokenServiceAccountValidationError } from '@/lib/credentials/token-serv
1818

1919
const {
2020
mockCheckWorkspaceAccess,
21-
mockGetCredentialActorContext,
2221
mockGetCredentialCreationWorkspaceContext,
2322
mockVerifyAndBuildServiceAccountSecret,
2423
} = vi.hoisted(() => ({
2524
mockCheckWorkspaceAccess: vi.fn(),
26-
mockGetCredentialActorContext: vi.fn(),
2725
mockGetCredentialCreationWorkspaceContext: vi.fn(),
2826
mockVerifyAndBuildServiceAccountSecret: vi.fn(),
2927
}))
@@ -43,10 +41,6 @@ vi.mock('@/lib/credentials/oauth', () => ({
4341
syncWorkspaceOAuthCredentialsForUser: vi.fn(),
4442
}))
4543

46-
vi.mock('@/lib/credentials/access', () => ({
47-
getCredentialActorContext: mockGetCredentialActorContext,
48-
}))
49-
5044
vi.mock('@/lib/oauth', () => ({
5145
getServiceConfigByProviderId: vi.fn(),
5246
}))
@@ -138,15 +132,9 @@ describe('POST /api/credentials', () => {
138132
memberUserIds: ['user-1'],
139133
canWrite: true,
140134
})
141-
mockGetCredentialActorContext.mockResolvedValue({
142-
member: null,
143-
hasWorkspaceAccess: true,
144-
canWriteWorkspace: true,
145-
isAdmin: true,
146-
})
147135
})
148136

149-
describe('service-account credentials', () => {
137+
describe('client-credential service accounts', () => {
150138
it('forwards clientId, clientSecret, and orgId to the secret builder on create', async () => {
151139
mockVerifyAndBuildServiceAccountSecret.mockResolvedValueOnce({
152140
providerId: 'zoom-service-account',
@@ -252,136 +240,6 @@ describe('POST /api/credentials', () => {
252240
)
253241
})
254242

255-
it.each([
256-
{
257-
family: 'client-credential',
258-
providerId: 'zoom-service-account',
259-
fields: {
260-
orgId: 'zoom-account',
261-
clientId: 'new-client-id',
262-
clientSecret: 'new-client-secret',
263-
},
264-
},
265-
{
266-
family: 'token-paste',
267-
providerId: 'snowflake-service-account',
268-
fields: {
269-
domain: 'acme.snowflakecomputing.com',
270-
apiToken: 'new-programmatic-access-token',
271-
},
272-
},
273-
{
274-
family: 'Google JSON-key',
275-
providerId: 'google-service-account',
276-
fields: {
277-
serviceAccountJson: JSON.stringify({
278-
type: 'service_account',
279-
client_email: 'automation@example.iam.gserviceaccount.com',
280-
private_key: 'new-private-key',
281-
project_id: 'example-project',
282-
}),
283-
},
284-
},
285-
{
286-
family: 'Atlassian',
287-
providerId: 'atlassian-service-account',
288-
fields: { domain: 'example.atlassian.net', apiToken: 'new-atlassian-token' },
289-
},
290-
{
291-
family: 'Slack different-ID',
292-
providerId: 'slack-custom-bot',
293-
fields: {
294-
id: '33333333-4444-4555-8666-777777777777',
295-
signingSecret: 'new-slack-signing-secret',
296-
botToken: 'xoxb-new-slack-bot-token',
297-
},
298-
},
299-
])(
300-
'conflicts for an existing $family service account instead of dropping new secrets',
301-
async ({ providerId, fields }) => {
302-
const displayName = 'Production automation'
303-
mockVerifyAndBuildServiceAccountSecret.mockResolvedValueOnce({
304-
providerId,
305-
encryptedServiceAccountKey: 'new-secret-ciphertext',
306-
displayName,
307-
auditMetadata: {},
308-
principal: null,
309-
})
310-
queueTableRows(credential, [
311-
{
312-
id: 'existing-credential',
313-
workspaceId: WORKSPACE_ID,
314-
type: 'service_account',
315-
displayName,
316-
providerId,
317-
},
318-
])
319-
320-
const response = await POST(
321-
createMockRequest('POST', {
322-
workspaceId: WORKSPACE_ID,
323-
type: 'service_account',
324-
providerId,
325-
displayName,
326-
...fields,
327-
})
328-
)
329-
const body = await response.json()
330-
331-
expect(response.status).toBe(409)
332-
expect(body.code).toBe('duplicate_display_name')
333-
expect(dbChainMockFns.update).not.toHaveBeenCalled()
334-
expect(dbChainMockFns.insert).not.toHaveBeenCalled()
335-
}
336-
)
337-
338-
it('preserves an exact-ID Slack custom-bot create replay', async () => {
339-
const credentialId = '22222222-3333-4444-8555-666666666666'
340-
const existing = {
341-
id: credentialId,
342-
workspaceId: WORKSPACE_ID,
343-
type: 'service_account',
344-
displayName: 'Production Slack Bot',
345-
description: null,
346-
providerId: 'slack-custom-bot',
347-
accountId: null,
348-
envKey: null,
349-
envOwnerUserId: null,
350-
encryptedServiceAccountKey: 'existing-secret-ciphertext',
351-
createdBy: 'user-1',
352-
createdAt: new Date('2026-08-11T00:00:00.000Z'),
353-
updatedAt: new Date('2026-08-11T00:00:00.000Z'),
354-
}
355-
mockVerifyAndBuildServiceAccountSecret.mockResolvedValueOnce({
356-
providerId: 'slack-custom-bot',
357-
encryptedServiceAccountKey: 'replayed-secret-ciphertext',
358-
displayName: existing.displayName,
359-
auditMetadata: {},
360-
principal: null,
361-
})
362-
queueTableRows(credential, [existing])
363-
364-
const response = await POST(
365-
createMockRequest('POST', {
366-
id: credentialId,
367-
workspaceId: WORKSPACE_ID,
368-
type: 'service_account',
369-
providerId: 'slack-custom-bot',
370-
displayName: existing.displayName,
371-
signingSecret: 'slack-signing-secret',
372-
botToken: 'xoxb-slack-bot-token',
373-
})
374-
)
375-
376-
const body = await response.json()
377-
expect({ status: response.status, body }).toMatchObject({
378-
status: 200,
379-
body: { credential: { id: credentialId } },
380-
})
381-
expect(dbChainMockFns.update).not.toHaveBeenCalled()
382-
expect(dbChainMockFns.insert).not.toHaveBeenCalled()
383-
})
384-
385243
it('maps a verification failure to a 400 with the validation code', async () => {
386244
mockVerifyAndBuildServiceAccountSecret.mockRejectedValueOnce(
387245
new TokenServiceAccountValidationError('invalid_credentials', 400, {

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/selector-input/selector-input.tsx

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import { useEffect, useRef } from 'react'
44
import { Tooltip } from '@sim/emcn'
5-
import type { CanonicalModeOverrides } from '@/lib/workflows/subblocks/visibility'
65
import { SelectorCombobox } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/selector-combobox/selector-combobox'
76
import { useSelectorSetup } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-selector-setup'
87
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
@@ -22,7 +21,6 @@ interface SelectorInputProps {
2221
isPreview?: boolean
2322
previewValue?: any
2423
previewContextValues?: Record<string, any>
25-
canonicalModeOverrides?: CanonicalModeOverrides
2624
overrides?: SelectorOverrides
2725
}
2826

@@ -33,7 +31,6 @@ export function SelectorInput({
3331
isPreview = false,
3432
previewValue,
3533
previewContextValues,
36-
canonicalModeOverrides,
3734
overrides,
3835
}: SelectorInputProps) {
3936
const { collaborativeSetSubblockValue } = useCollaborativeWorkflow()
@@ -46,12 +43,7 @@ export function SelectorInput({
4643
allowSearch,
4744
disabled: selectorDisabled,
4845
dependencyValues,
49-
} = useSelectorSetup(blockId, subBlock, {
50-
disabled,
51-
isPreview,
52-
previewContextValues,
53-
canonicalModeOverrides,
54-
})
46+
} = useSelectorSetup(blockId, subBlock, { disabled, isPreview, previewContextValues })
5547

5648
const selectorContext = overrides?.transformContext
5749
? overrides.transformContext(autoContext, dependencyValues)

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/sub-block-renderer.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
'use client'
22

33
import { useCallback, useEffect, useRef } from 'react'
4-
import type { CanonicalModeOverrides } from '@/lib/workflows/subblocks/visibility'
54
import {
65
buildToolSubBlockId,
76
resolveToolParamSync,
@@ -24,7 +23,6 @@ interface ToolSubBlockRendererProps {
2423
toolParams: Record<string, string> | undefined
2524
onParamChange: (toolIndex: number, paramId: string, value: string) => void
2625
disabled: boolean
27-
canonicalModeOverrides?: CanonicalModeOverrides
2826
canonicalToggle?: {
2927
mode: 'basic' | 'advanced'
3028
disabled?: boolean
@@ -63,7 +61,6 @@ export function ToolSubBlockRenderer({
6361
toolParams,
6462
onParamChange,
6563
disabled,
66-
canonicalModeOverrides,
6764
canonicalToggle,
6865
}: ToolSubBlockRendererProps) {
6966
const syntheticId = buildToolSubBlockId(subBlockId, toolIndex, effectiveParamId)
@@ -143,7 +140,6 @@ export function ToolSubBlockRenderer({
143140
disabled={disabled}
144141
canonicalToggle={canonicalToggle}
145142
dependencyContext={toolParams}
146-
canonicalModeOverrides={canonicalModeOverrides}
147143
/>
148144
</DependencyBlockTypeProvider>
149145
)

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2151,7 +2151,6 @@ export const ToolInput = memo(function ToolInput({
21512151
toolParams={tool.params}
21522152
onParamChange={handleParamChange}
21532153
disabled={disabled}
2154-
canonicalModeOverrides={toolScopedOverrides}
21552154
canonicalToggle={canonicalToggleProp}
21562155
/>
21572156
</ActiveSearchTargetProvider>

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate.ts

Lines changed: 19 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,9 @@ import { isEqual } from 'es-toolkit'
55
import { useStoreWithEqualityFn } from 'zustand/traditional'
66
import {
77
buildCanonicalIndex,
8-
type CanonicalModeOverrides,
98
isNonEmptyValue,
109
normalizeDependencyValue,
1110
parseDependsOn,
12-
resolveActiveDependencyValue,
1311
resolveDependencyValue,
1412
} from '@/lib/workflows/subblocks/visibility'
1513
import { getBlock } from '@/blocks/registry'
@@ -28,18 +26,11 @@ import { useDependencyBlockType } from './use-dependency-block-type'
2826
export function useDependsOnGate(
2927
blockId: string,
3028
subBlock: SubBlockConfig,
31-
opts?: {
32-
disabled?: boolean
33-
isPreview?: boolean
34-
previewContextValues?: Record<string, any>
35-
canonicalModeOverrides?: CanonicalModeOverrides
36-
strictCanonicalDependencies?: boolean
37-
}
29+
opts?: { disabled?: boolean; isPreview?: boolean; previewContextValues?: Record<string, any> }
3830
) {
3931
const disabledProp = opts?.disabled ?? false
4032
const isPreview = opts?.isPreview ?? false
4133
const previewContextValues = opts?.previewContextValues
42-
const strictCanonicalDependencies = opts?.strictCanonicalDependencies ?? false
4334

4435
const activeWorkflowId = useWorkflowRegistry((s) => s.activeWorkflowId)
4536
const blockState = useWorkflowStore((state) => state.blocks[blockId])
@@ -54,7 +45,7 @@ export function useDependsOnGate(
5445
() => buildCanonicalIndex(blockConfig?.subBlocks || []),
5546
[blockConfig?.subBlocks]
5647
)
57-
const canonicalModeOverrides = opts?.canonicalModeOverrides ?? blockState?.data?.canonicalModes
48+
const canonicalModeOverrides = blockState?.data?.canonicalModes
5849

5950
// Parse dependsOn config to get all/any field lists
6051
const { allFields, anyFields, allDependsOnFields } = useMemo(
@@ -69,17 +60,17 @@ export function useDependsOnGate(
6960
(state: ReturnType<typeof useSubBlockStore.getState>) => {
7061
if (allDependsOnFields.length === 0) return {} as Record<string, unknown>
7162

72-
const resolveValue = strictCanonicalDependencies
73-
? resolveActiveDependencyValue
74-
: resolveDependencyValue
75-
7663
// If previewContextValues are provided (e.g., tool parameters), use those first
7764
if (previewContextValues) {
7865
const map: Record<string, unknown> = {}
7966
for (const key of allDependsOnFields) {
80-
map[key] = normalizeDependencyValue(
81-
resolveValue(key, previewContextValues, canonicalIndex, canonicalModeOverrides)
67+
const resolvedValue = resolveDependencyValue(
68+
key,
69+
previewContextValues,
70+
canonicalIndex,
71+
canonicalModeOverrides
8272
)
73+
map[key] = normalizeDependencyValue(resolvedValue)
8374
}
8475
return map
8576
}
@@ -96,9 +87,13 @@ export function useDependsOnGate(
9687
const blockValues = (workflowValues as any)[blockId] || {}
9788
const map: Record<string, unknown> = {}
9889
for (const key of allDependsOnFields) {
99-
map[key] = normalizeDependencyValue(
100-
resolveValue(key, blockValues, canonicalIndex, canonicalModeOverrides)
90+
const resolvedValue = resolveDependencyValue(
91+
key,
92+
blockValues,
93+
canonicalIndex,
94+
canonicalModeOverrides
10195
)
96+
map[key] = normalizeDependencyValue(resolvedValue)
10297
}
10398
return map
10499
},
@@ -109,25 +104,24 @@ export function useDependsOnGate(
109104
blockId,
110105
canonicalIndex,
111106
canonicalModeOverrides,
112-
strictCanonicalDependencies,
113107
]
114108
)
115109

116110
// Get values for all dependency fields (both all and any)
117111
// Use isEqual to prevent re-renders when dependency values haven't actually changed
118-
const dependencyValues = useStoreWithEqualityFn(useSubBlockStore, dependencySelector, isEqual)
112+
const dependencyValuesMap = useStoreWithEqualityFn(useSubBlockStore, dependencySelector, isEqual)
119113

120114
const depsSatisfied = useMemo(() => {
121115
// Check all fields (AND logic) - all must be satisfied
122116
const allSatisfied =
123-
allFields.length === 0 || allFields.every((key) => isNonEmptyValue(dependencyValues[key]))
117+
allFields.length === 0 || allFields.every((key) => isNonEmptyValue(dependencyValuesMap[key]))
124118

125119
// Check any fields (OR logic) - at least one must be satisfied
126120
const anySatisfied =
127-
anyFields.length === 0 || anyFields.some((key) => isNonEmptyValue(dependencyValues[key]))
121+
anyFields.length === 0 || anyFields.some((key) => isNonEmptyValue(dependencyValuesMap[key]))
128122

129123
return allSatisfied && anySatisfied
130-
}, [allFields, anyFields, dependencyValues])
124+
}, [allFields, anyFields, dependencyValuesMap])
131125

132126
// Block everything except the credential field itself until dependencies are set
133127
const blocked =
@@ -140,7 +134,7 @@ export function useDependsOnGate(
140134
depsSatisfied,
141135
blocked,
142136
finalDisabled,
143-
dependencyValues,
137+
dependencyValues: dependencyValuesMap,
144138
canonicalIndex,
145139
}
146140
}

0 commit comments

Comments
 (0)