Skip to content

Commit 0765710

Browse files
fix(integrations): address enrichment review findings
1 parent 02d1783 commit 0765710

27 files changed

Lines changed: 637 additions & 82 deletions

apps/docs/components/icons.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9839,7 +9839,7 @@ export function SmarteIcon(props: SVGProps<SVGSVGElement>) {
98399839
return (
98409840
<svg
98419841
{...props}
9842-
viewBox='0 0 44 44'
9842+
viewBox='-1 -1 46 46'
98439843
fill='currentColor'
98449844
xmlns='http://www.w3.org/2000/svg'
98459845
aria-hidden='true'
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { describe, expect, it } from 'vitest'
5+
import { ForagerBlock } from '@/blocks/blocks/forager'
6+
7+
function mapParams(params: Record<string, unknown>): Record<string, unknown> {
8+
const mapper = ForagerBlock.tools.config.params
9+
if (!mapper) throw new Error('Forager block is missing tools.config.params')
10+
return mapper(params)
11+
}
12+
13+
function subBlock(id: string) {
14+
const config = ForagerBlock.subBlocks.find((candidate) => candidate.id === id)
15+
if (!config) throw new Error(`Forager block is missing ${id}`)
16+
return config
17+
}
18+
19+
describe('Forager block lookup requirements', () => {
20+
it('keeps alternate identifiers optional and explains the one-of requirements', () => {
21+
for (const id of ['personId', 'linkedinPublicIdentifier']) {
22+
expect(subBlock(id).required).toBe(false)
23+
expect(subBlock(id).description).toContain('Required unless')
24+
}
25+
for (const id of ['domain', 'organizationId', 'organizationLinkedinPublicIdentifier']) {
26+
expect(subBlock(id).required).toBe(false)
27+
expect(subBlock(id).description).toContain('Required unless')
28+
}
29+
})
30+
31+
it.each([
32+
'forager_person_personal_emails',
33+
'forager_person_phone_numbers',
34+
'forager_person_work_emails',
35+
'forager_person_detail',
36+
])('rejects %s before the handler when both person identifiers are missing', (operation) => {
37+
expect(() => mapParams({ operation, personId: null, linkedinPublicIdentifier: ' ' })).toThrow(
38+
/requires Person ID or LinkedIn Public Identifier/
39+
)
40+
})
41+
42+
it('accepts either person identifier', () => {
43+
expect(
44+
mapParams({ operation: 'forager_person_detail', linkedinPublicIdentifier: 'jane-doe' })
45+
).toEqual({ linkedinPublicIdentifier: 'jane-doe' })
46+
expect(mapParams({ operation: 'forager_person_detail', personId: '42' })).toEqual({
47+
personId: 42,
48+
})
49+
})
50+
51+
it('rejects Website Detail before the handler when every lookup field is missing', () => {
52+
expect(() =>
53+
mapParams({
54+
operation: 'forager_website_detail',
55+
domain: ' ',
56+
organizationId: null,
57+
organizationLinkedinPublicIdentifier: '',
58+
})
59+
).toThrow(/requires Domain, Organization ID, or Organization LinkedIn Public Identifier/)
60+
})
61+
62+
it('accepts each Website Detail lookup alternative', () => {
63+
expect(mapParams({ operation: 'forager_website_detail', domain: 'example.com' })).toEqual({
64+
domain: 'example.com',
65+
})
66+
expect(mapParams({ operation: 'forager_website_detail', organizationId: '42' })).toEqual({
67+
organizationId: 42,
68+
})
69+
expect(
70+
mapParams({
71+
operation: 'forager_website_detail',
72+
organizationLinkedinPublicIdentifier: 'example-company',
73+
})
74+
).toEqual({ organizationLinkedinPublicIdentifier: 'example-company' })
75+
})
76+
})

apps/sim/blocks/blocks/forager.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,36 @@ function parseJsonObject(value: unknown, fieldName: string): Record<string, unkn
4343
}
4444
}
4545

46+
function hasLookupValue(value: unknown): boolean {
47+
if (value === undefined || value === null) return false
48+
return typeof value !== 'string' || value.trim().length > 0
49+
}
50+
51+
function assertLookupRequirements(operation: unknown, params: Record<string, unknown>): void {
52+
if (
53+
typeof operation === 'string' &&
54+
PERSON_IDENTIFIER_OPERATIONS.includes(
55+
operation as (typeof PERSON_IDENTIFIER_OPERATIONS)[number]
56+
)
57+
) {
58+
if (!hasLookupValue(params.personId) && !hasLookupValue(params.linkedinPublicIdentifier)) {
59+
throw new Error('Forager person lookup requires Person ID or LinkedIn Public Identifier')
60+
}
61+
}
62+
63+
if (operation === 'forager_website_detail') {
64+
if (
65+
!hasLookupValue(params.domain) &&
66+
!hasLookupValue(params.organizationId) &&
67+
!hasLookupValue(params.organizationLinkedinPublicIdentifier)
68+
) {
69+
throw new Error(
70+
'Forager website lookup requires Domain, Organization ID, or Organization LinkedIn Public Identifier'
71+
)
72+
}
73+
}
74+
}
75+
4676
export const ForagerBlock: BlockConfig<ForagerResponse> = {
4777
type: 'forager',
4878
name: 'Forager',
@@ -186,13 +216,17 @@ export const ForagerBlock: BlockConfig<ForagerResponse> = {
186216
title: 'Person ID',
187217
type: 'short-input',
188218
placeholder: '12345',
219+
description: 'Required unless LinkedIn Public Identifier is provided',
220+
required: false,
189221
condition: { field: 'operation', value: [...PERSON_IDENTIFIER_OPERATIONS] },
190222
},
191223
{
192224
id: 'linkedinPublicIdentifier',
193225
title: 'LinkedIn Public Identifier',
194226
type: 'short-input',
195227
placeholder: 'jane-doe',
228+
description: 'Required unless Person ID is provided',
229+
required: false,
196230
condition: { field: 'operation', value: [...PERSON_IDENTIFIER_OPERATIONS] },
197231
},
198232
{
@@ -228,13 +262,18 @@ export const ForagerBlock: BlockConfig<ForagerResponse> = {
228262
title: 'Domain',
229263
type: 'short-input',
230264
placeholder: 'example.com',
265+
description:
266+
'Required unless Organization ID or Organization LinkedIn Public Identifier is provided',
267+
required: false,
231268
condition: { field: 'operation', value: 'forager_website_detail' },
232269
},
233270
{
234271
id: 'organizationId',
235272
title: 'Organization ID',
236273
type: 'short-input',
237274
placeholder: '12345',
275+
description: 'Required unless Domain or Organization LinkedIn Public Identifier is provided',
276+
required: false,
238277
condition: { field: 'operation', value: 'forager_website_detail' },
239278
mode: 'advanced',
240279
},
@@ -243,6 +282,8 @@ export const ForagerBlock: BlockConfig<ForagerResponse> = {
243282
title: 'Organization LinkedIn Public Identifier',
244283
type: 'short-input',
245284
placeholder: 'example-company',
285+
description: 'Required unless Domain or Organization ID is provided',
286+
required: false,
246287
condition: { field: 'operation', value: 'forager_website_detail' },
247288
mode: 'advanced',
248289
},
@@ -293,6 +334,7 @@ export const ForagerBlock: BlockConfig<ForagerResponse> = {
293334
result[key] = value
294335
}
295336
}
337+
assertLookupRequirements(params.operation, result)
296338
return result
297339
},
298340
},

apps/sim/blocks/blocks/kitt.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,51 @@ describe('Kitt block tool wiring', () => {
5959
})
6060
})
6161

62+
it('excludes inactive verifier fields from finder requests', () => {
63+
expect(
64+
mapParams({
65+
operation: 'kitt_find_email',
66+
apiKey: 'test-key',
67+
fe_fullName: 'Erol Toker',
68+
fe_domain: 'trykitt.ai',
69+
fe_customData: 'finder-metadata',
70+
ve_email: 'stale@example.com',
71+
ve_treatAliasesAsValid: 'true',
72+
ve_customData: 'stale-verifier-metadata',
73+
})
74+
).toEqual({
75+
apiKey: 'test-key',
76+
fullName: 'Erol Toker',
77+
domain: 'trykitt.ai',
78+
customData: 'finder-metadata',
79+
})
80+
})
81+
82+
it('excludes inactive finder fields from verifier requests', () => {
83+
expect(
84+
mapParams({
85+
operation: 'kitt_verify_email',
86+
apiKey: 'test-key',
87+
fe_fullName: 'Stale Person',
88+
fe_domain: 'stale.example',
89+
fe_strictNameMatches: 'true',
90+
fe_customData: 'stale-finder-metadata',
91+
ve_email: 'erol@trykitt.ai',
92+
ve_customData: 'verifier-metadata',
93+
})
94+
).toEqual({
95+
apiKey: 'test-key',
96+
email: 'erol@trykitt.ai',
97+
customData: 'verifier-metadata',
98+
})
99+
})
100+
101+
it('fails fast when parameter mapping receives an unknown operation', () => {
102+
expect(() => mapParams({ operation: 'unsupported', apiKey: 'test-key' })).toThrow(
103+
/Unsupported Kitt operation/
104+
)
105+
})
106+
62107
it('hides the required API key field on hosted Sim', () => {
63108
const apiKey = KittBlock.subBlocks.find((subBlock) => subBlock.id === 'apiKey')
64109
expect(apiKey).toMatchObject({ required: true, password: true, hideWhenHosted: true })

apps/sim/blocks/blocks/kitt.ts

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@ import { KittIcon } from '@/components/icons'
22
import { AuthMode, type BlockConfig, type BlockMeta, IntegrationType } from '@/blocks/types'
33
import type { KittResponse } from '@/tools/kitt/types'
44

5+
function removeEmptyParams(params: Record<string, unknown>): Record<string, unknown> {
6+
const result: Record<string, unknown> = {}
7+
for (const [key, value] of Object.entries(params)) {
8+
if (value !== undefined && value !== null && value !== '') result[key] = value
9+
}
10+
return result
11+
}
12+
513
export const KittBlock: BlockConfig<KittResponse> = {
614
type: 'kitt',
715
name: 'Kitt',
@@ -128,27 +136,31 @@ export const KittBlock: BlockConfig<KittResponse> = {
128136
throw new Error(`Unsupported Kitt operation: ${String(params.operation)}`)
129137
},
130138
params: (params) => {
131-
const { operation: _operation, ...rest } = params
132-
const idToParam: Record<string, string> = {
133-
fe_customData: 'customData',
134-
fe_domain: 'domain',
135-
fe_fullName: 'fullName',
136-
fe_linkedinStandardProfileURL: 'linkedinStandardProfileURL',
137-
fe_strictNameMatches: 'strictNameMatches',
138-
ve_customData: 'customData',
139-
ve_email: 'email',
140-
ve_treatAliasesAsValid: 'treatAliasesAsValid',
139+
const result: Record<string, unknown> = {
140+
apiKey: params.apiKey,
141141
}
142-
const booleanFields = new Set(['strictNameMatches', 'treatAliasesAsValid'])
143-
const result: Record<string, unknown> = {}
144-
for (const [key, value] of Object.entries(rest)) {
145-
if (value === undefined || value === null || value === '') continue
146-
const mappedKey = idToParam[key] ?? key
147-
result[mappedKey] = booleanFields.has(mappedKey)
148-
? value === true || value === 'true'
149-
: value
142+
143+
if (params.operation === 'kitt_find_email') {
144+
result.fullName = params.fe_fullName
145+
result.domain = params.fe_domain
146+
result.linkedinStandardProfileURL = params.fe_linkedinStandardProfileURL
147+
result.customData = params.fe_customData
148+
if (params.fe_strictNameMatches !== undefined && params.fe_strictNameMatches !== '') {
149+
result.strictNameMatches =
150+
params.fe_strictNameMatches === true || params.fe_strictNameMatches === 'true'
151+
}
152+
} else if (params.operation === 'kitt_verify_email') {
153+
result.email = params.ve_email
154+
result.customData = params.ve_customData
155+
if (params.ve_treatAliasesAsValid !== undefined && params.ve_treatAliasesAsValid !== '') {
156+
result.treatAliasesAsValid =
157+
params.ve_treatAliasesAsValid === true || params.ve_treatAliasesAsValid === 'true'
158+
}
159+
} else {
160+
throw new Error(`Unsupported Kitt operation: ${String(params.operation)}`)
150161
}
151-
return result
162+
163+
return removeEmptyParams(result)
152164
},
153165
},
154166
},
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { describe, expect, it } from 'vitest'
5+
import { ZeliqBlock } from '@/blocks/blocks/zeliq'
6+
7+
function mapParams(params: Record<string, unknown>): Record<string, unknown> {
8+
const mapper = ZeliqBlock.tools.config.params
9+
if (!mapper) throw new Error('Zeliq block is missing tools.config.params')
10+
return mapper(params)
11+
}
12+
13+
describe('Zeliq block tool wiring', () => {
14+
it('maps only the active LinkedIn email lookup fields', () => {
15+
expect(
16+
mapParams({
17+
operation: 'zeliq_enrich_email',
18+
emailLookupMethod: 'linkedin',
19+
apiKey: 'test-key',
20+
callbackUrl: 'https://example.com/callback',
21+
emailLinkedInUrl: 'https://linkedin.com/in/active',
22+
emailFirstName: 'Stale',
23+
emailLastName: 'Person',
24+
emailDomain: 'stale.example',
25+
phoneLookupMethod: 'email',
26+
phoneEmail: 'stale@example.com',
27+
})
28+
).toEqual({
29+
apiKey: 'test-key',
30+
callbackUrl: 'https://example.com/callback',
31+
linkedinUrl: 'https://linkedin.com/in/active',
32+
})
33+
})
34+
35+
it('maps only the active person-details email lookup fields', () => {
36+
expect(
37+
mapParams({
38+
operation: 'zeliq_enrich_email',
39+
emailLookupMethod: 'person_details',
40+
apiKey: 'test-key',
41+
callbackUrl: 'https://example.com/callback',
42+
emailLinkedInUrl: 'https://linkedin.com/in/stale',
43+
emailFirstName: 'Jane',
44+
emailLastName: 'Doe',
45+
emailCompany: 'Example Inc',
46+
emailDomain: 'example.com',
47+
phoneLinkedInUrl: 'https://linkedin.com/in/stale-phone',
48+
})
49+
).toEqual({
50+
apiKey: 'test-key',
51+
callbackUrl: 'https://example.com/callback',
52+
firstName: 'Jane',
53+
lastName: 'Doe',
54+
company: 'Example Inc',
55+
domain: 'example.com',
56+
})
57+
})
58+
59+
it('maps only the active phone lookup fields', () => {
60+
expect(
61+
mapParams({
62+
operation: 'zeliq_enrich_phone',
63+
phoneLookupMethod: 'email',
64+
apiKey: 'test-key',
65+
callbackUrl: 'https://example.com/callback',
66+
phoneEmail: 'active@example.com',
67+
phoneLinkedInUrl: 'https://linkedin.com/in/stale-phone',
68+
emailLookupMethod: 'linkedin',
69+
emailLinkedInUrl: 'https://linkedin.com/in/stale-email',
70+
})
71+
).toEqual({
72+
apiKey: 'test-key',
73+
callbackUrl: 'https://example.com/callback',
74+
email: 'active@example.com',
75+
})
76+
})
77+
78+
it('fails fast for unknown operations and lookup methods', () => {
79+
expect(() => mapParams({ operation: 'unsupported' })).toThrow(/Unsupported Zeliq operation/)
80+
expect(() =>
81+
mapParams({ operation: 'zeliq_enrich_email', emailLookupMethod: 'unsupported' })
82+
).toThrow(/Unsupported Zeliq email lookup method/)
83+
expect(() =>
84+
mapParams({ operation: 'zeliq_enrich_phone', phoneLookupMethod: 'unsupported' })
85+
).toThrow(/Unsupported Zeliq phone lookup method/)
86+
})
87+
})

0 commit comments

Comments
 (0)