Skip to content

Commit 5956730

Browse files
committed
fix(grain): update to stable version of API
1 parent fdd587d commit 5956730

File tree

19 files changed

+336
-191
lines changed

19 files changed

+336
-191
lines changed

apps/sim/app/api/webhooks/trigger/[path]/route.test.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,7 @@ vi.mock('@/lib/core/utils/request', () => requestUtilsMock)
353353

354354
process.env.DATABASE_URL = 'postgresql://test:test@localhost:5432/test'
355355

356-
import { POST } from '@/app/api/webhooks/trigger/[path]/route'
356+
import { GET, POST } from '@/app/api/webhooks/trigger/[path]/route'
357357

358358
describe('Webhook Trigger API Route', () => {
359359
beforeEach(() => {
@@ -401,6 +401,44 @@ describe('Webhook Trigger API Route', () => {
401401
expect(text).toMatch(/not found/i)
402402
})
403403

404+
it('should return 200 for GET verification probes before webhook persistence', async () => {
405+
const req = createMockRequest(
406+
'GET',
407+
undefined,
408+
{},
409+
'http://localhost:3000/api/webhooks/trigger/non-existent-path'
410+
)
411+
412+
const params = Promise.resolve({ path: 'non-existent-path' })
413+
414+
const response = await GET(req as any, { params })
415+
416+
expect(response.status).toBe(200)
417+
await expect(response.json()).resolves.toMatchObject({
418+
status: 'ok',
419+
message: 'Webhook endpoint verified',
420+
})
421+
})
422+
423+
it('should return 200 for empty POST verification probes before webhook persistence', async () => {
424+
const req = createMockRequest(
425+
'POST',
426+
undefined,
427+
{},
428+
'http://localhost:3000/api/webhooks/trigger/non-existent-path'
429+
)
430+
431+
const params = Promise.resolve({ path: 'non-existent-path' })
432+
433+
const response = await POST(req as any, { params })
434+
435+
expect(response.status).toBe(200)
436+
await expect(response.json()).resolves.toMatchObject({
437+
status: 'ok',
438+
message: 'Webhook endpoint verified',
439+
})
440+
})
441+
404442
describe('Generic Webhook Authentication', () => {
405443
it('should process generic webhook without authentication', async () => {
406444
testData.webhooks.push({

apps/sim/app/api/webhooks/trigger/[path]/route.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
checkWebhookPreprocessing,
66
findAllWebhooksForPath,
77
handlePreDeploymentVerification,
8+
handlePreLookupWebhookVerification,
89
handleProviderChallenges,
910
handleProviderReachabilityTest,
1011
parseWebhookBody,
@@ -30,7 +31,12 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
3031
return challengeResponse
3132
}
3233

33-
return new NextResponse('Method not allowed', { status: 405 })
34+
return handlePreLookupWebhookVerification(
35+
request.method,
36+
undefined,
37+
requestId,
38+
path
39+
) as NextResponse
3440
}
3541

3642
export async function POST(
@@ -64,6 +70,16 @@ export async function POST(
6470
const webhooksForPath = await findAllWebhooksForPath({ requestId, path })
6571

6672
if (webhooksForPath.length === 0) {
73+
const verificationResponse = handlePreLookupWebhookVerification(
74+
request.method,
75+
body,
76+
requestId,
77+
path
78+
)
79+
if (verificationResponse) {
80+
return verificationResponse
81+
}
82+
6783
logger.warn(`[${requestId}] Webhook or workflow not found for path: ${path}`)
6884
return new NextResponse('Not Found', { status: 404 })
6985
}

apps/sim/blocks/blocks/grain.ts

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export const GrainBlock: BlockConfig = {
2525
{ label: 'List Recordings', id: 'grain_list_recordings' },
2626
{ label: 'Get Recording', id: 'grain_get_recording' },
2727
{ label: 'Get Transcript', id: 'grain_get_transcript' },
28+
{ label: 'List Views', id: 'grain_list_views' },
2829
{ label: 'List Teams', id: 'grain_list_teams' },
2930
{ label: 'List Meeting Types', id: 'grain_list_meeting_types' },
3031
{ label: 'Create Webhook', id: 'grain_create_hook' },
@@ -72,7 +73,7 @@ export const GrainBlock: BlockConfig = {
7273
placeholder: 'ISO8601 timestamp (e.g., 2024-01-01T00:00:00Z)',
7374
condition: {
7475
field: 'operation',
75-
value: ['grain_list_recordings', 'grain_create_hook'],
76+
value: ['grain_list_recordings'],
7677
},
7778
wandConfig: {
7879
enabled: true,
@@ -96,7 +97,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
9697
placeholder: 'ISO8601 timestamp (e.g., 2024-01-01T00:00:00Z)',
9798
condition: {
9899
field: 'operation',
99-
value: ['grain_list_recordings', 'grain_create_hook'],
100+
value: ['grain_list_recordings'],
100101
},
101102
wandConfig: {
102103
enabled: true,
@@ -125,7 +126,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
125126
value: () => '',
126127
condition: {
127128
field: 'operation',
128-
value: ['grain_list_recordings', 'grain_create_hook'],
129+
value: ['grain_list_recordings'],
129130
},
130131
},
131132
// Title search
@@ -162,7 +163,7 @@ Return ONLY the search term - no explanations, no quotes, no extra text.`,
162163
placeholder: 'Filter by team UUID (optional)',
163164
condition: {
164165
field: 'operation',
165-
value: ['grain_list_recordings', 'grain_create_hook'],
166+
value: ['grain_list_recordings'],
166167
},
167168
},
168169
// Meeting type ID filter
@@ -173,7 +174,7 @@ Return ONLY the search term - no explanations, no quotes, no extra text.`,
173174
placeholder: 'Filter by meeting type UUID (optional)',
174175
condition: {
175176
field: 'operation',
176-
value: ['grain_list_recordings', 'grain_create_hook'],
177+
value: ['grain_list_recordings'],
177178
},
178179
},
179180
// Include highlights
@@ -183,7 +184,7 @@ Return ONLY the search term - no explanations, no quotes, no extra text.`,
183184
type: 'switch',
184185
condition: {
185186
field: 'operation',
186-
value: ['grain_list_recordings', 'grain_get_recording', 'grain_create_hook'],
187+
value: ['grain_list_recordings', 'grain_get_recording'],
187188
},
188189
},
189190
// Include participants
@@ -193,7 +194,7 @@ Return ONLY the search term - no explanations, no quotes, no extra text.`,
193194
type: 'switch',
194195
condition: {
195196
field: 'operation',
196-
value: ['grain_list_recordings', 'grain_get_recording', 'grain_create_hook'],
197+
value: ['grain_list_recordings', 'grain_get_recording'],
197198
},
198199
},
199200
// Include AI summary
@@ -203,7 +204,18 @@ Return ONLY the search term - no explanations, no quotes, no extra text.`,
203204
type: 'switch',
204205
condition: {
205206
field: 'operation',
206-
value: ['grain_list_recordings', 'grain_get_recording', 'grain_create_hook'],
207+
value: ['grain_list_recordings', 'grain_get_recording'],
208+
},
209+
},
210+
{
211+
id: 'viewId',
212+
title: 'View ID',
213+
type: 'short-input',
214+
placeholder: 'Enter Grain view UUID',
215+
required: true,
216+
condition: {
217+
field: 'operation',
218+
value: ['grain_create_hook'],
207219
},
208220
},
209221
// Include calendar event (get_recording only)
@@ -271,6 +283,7 @@ Return ONLY the search term - no explanations, no quotes, no extra text.`,
271283
'grain_list_recordings',
272284
'grain_get_recording',
273285
'grain_get_transcript',
286+
'grain_list_views',
274287
'grain_list_teams',
275288
'grain_list_meeting_types',
276289
'grain_create_hook',
@@ -327,24 +340,21 @@ Return ONLY the search term - no explanations, no quotes, no extra text.`,
327340

328341
case 'grain_list_teams':
329342
case 'grain_list_meeting_types':
343+
case 'grain_list_views':
330344
case 'grain_list_hooks':
331345
return baseParams
332346

333347
case 'grain_create_hook':
334348
if (!params.hookUrl?.trim()) {
335349
throw new Error('Webhook URL is required.')
336350
}
351+
if (!params.viewId?.trim()) {
352+
throw new Error('View ID is required.')
353+
}
337354
return {
338355
...baseParams,
339356
hookUrl: params.hookUrl.trim(),
340-
filterBeforeDatetime: params.beforeDatetime || undefined,
341-
filterAfterDatetime: params.afterDatetime || undefined,
342-
filterParticipantScope: params.participantScope || undefined,
343-
filterTeamId: params.teamId || undefined,
344-
filterMeetingTypeId: params.meetingTypeId || undefined,
345-
includeHighlights: params.includeHighlights || false,
346-
includeParticipants: params.includeParticipants || false,
347-
includeAiSummary: params.includeAiSummary || false,
357+
viewId: params.viewId.trim(),
348358
}
349359

350360
case 'grain_delete_hook':
@@ -367,6 +377,7 @@ Return ONLY the search term - no explanations, no quotes, no extra text.`,
367377
apiKey: { type: 'string', description: 'Grain API key (Personal Access Token)' },
368378
recordingId: { type: 'string', description: 'Recording UUID' },
369379
cursor: { type: 'string', description: 'Pagination cursor' },
380+
viewId: { type: 'string', description: 'Grain view UUID for webhook subscriptions' },
370381
beforeDatetime: {
371382
type: 'string',
372383
description: 'Filter recordings before this ISO8601 timestamp',
@@ -416,6 +427,7 @@ Return ONLY the search term - no explanations, no quotes, no extra text.`,
416427
teamsList: { type: 'json', description: 'Array of team objects' },
417428
// Meeting type outputs
418429
meetingTypes: { type: 'json', description: 'Array of meeting type objects' },
430+
views: { type: 'json', description: 'Array of Grain views' },
419431
// Hook outputs
420432
hooks: { type: 'json', description: 'Array of webhook objects' },
421433
hook: { type: 'json', description: 'Created webhook data' },

apps/sim/lib/webhooks/processor.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,32 @@ export async function handleProviderChallenges(
190190
return null
191191
}
192192

193+
/**
194+
* Returns a verification response for provider reachability probes that happen
195+
* before a webhook row exists and therefore before provider lookup is possible.
196+
*/
197+
export function handlePreLookupWebhookVerification(
198+
method: string,
199+
body: Record<string, unknown> | undefined,
200+
requestId: string,
201+
path: string
202+
): NextResponse | null {
203+
const isVerificationProbe =
204+
method === 'GET' ||
205+
method === 'HEAD' ||
206+
(method === 'POST' && (!body || Object.keys(body).length === 0))
207+
208+
if (!isVerificationProbe) {
209+
return null
210+
}
211+
212+
logger.info(
213+
`[${requestId}] Returning 200 for pre-lookup webhook verification probe on path: ${path}`
214+
)
215+
216+
return NextResponse.json({ status: 'ok', message: 'Webhook endpoint verified' })
217+
}
218+
193219
/**
194220
* Handle provider-specific reachability tests that occur AFTER webhook lookup.
195221
*

0 commit comments

Comments
 (0)