Skip to content

Commit ee7065d

Browse files
authored
feat: added Granola integration
* Granola * lint issue
1 parent d3a6229 commit ee7065d

18 files changed

Lines changed: 689 additions & 12 deletions

File tree

apps/bubble-studio/src/pages/CredentialsPage.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ const getServiceNameForCredentialType = (
117117
[CredentialType.METABASE_CRED]: 'Metabase',
118118
[CredentialType.CLERK_CRED]: 'Clerk',
119119
[CredentialType.CLERK_API_KEY]: 'Clerk',
120+
[CredentialType.GRANOLA_API_KEY]: 'Granola',
120121
};
121122

122123
return typeToServiceMap[credentialType] || credentialType;

packages/bubble-core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@bubblelab/bubble-core",
3-
"version": "0.1.289",
3+
"version": "0.1.291",
44
"type": "module",
55
"license": "Apache-2.0",
66
"main": "./dist/index.js",

packages/bubble-core/src/bubble-factory.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ export class BubbleFactory {
191191
'docusign',
192192
'metabase',
193193
'clerk',
194+
'granola',
194195
];
195196
}
196197

@@ -462,6 +463,9 @@ export class BubbleFactory {
462463
const { ClerkBubble } = await import(
463464
'./bubbles/service-bubble/clerk/index.js'
464465
);
466+
const { GranolaBubble } = await import(
467+
'./bubbles/service-bubble/granola/index.js'
468+
);
465469

466470
// Create the default factory instance
467471
this.register('hello-world', HelloWorldBubble as BubbleClassWithMetadata);
@@ -638,6 +642,7 @@ export class BubbleFactory {
638642
this.register('docusign', DocuSignBubble as BubbleClassWithMetadata);
639643
this.register('metabase', MetabaseBubble as BubbleClassWithMetadata);
640644
this.register('clerk', ClerkBubble as BubbleClassWithMetadata);
645+
this.register('granola', GranolaBubble as BubbleClassWithMetadata);
641646

642647
// After all default bubbles are registered, auto-populate bubbleDependencies
643648
if (!BubbleFactory.dependenciesPopulated) {
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import {
2+
BubbleFlow,
3+
GranolaBubble,
4+
type WebhookEvent,
5+
} from '@bubblelab/bubble-core';
6+
7+
export interface Output {
8+
noteId: string;
9+
testResults: {
10+
operation: string;
11+
success: boolean;
12+
details?: string;
13+
}[];
14+
}
15+
16+
export interface TestPayload extends WebhookEvent {
17+
testName?: string;
18+
}
19+
20+
export class GranolaIntegrationTest extends BubbleFlow<'webhook/http'> {
21+
async handle(payload: TestPayload): Promise<Output> {
22+
const results: Output['testResults'] = [];
23+
24+
// 1. List notes
25+
const listResult = await new GranolaBubble({
26+
operation: 'list_notes',
27+
page_size: 5,
28+
}).action();
29+
30+
results.push({
31+
operation: 'list_notes',
32+
success: listResult.success,
33+
details: listResult.success
34+
? `Retrieved ${listResult.notes?.length ?? 0} notes, hasMore: ${listResult.hasMore}`
35+
: listResult.error,
36+
});
37+
38+
// 2. List notes with date filter
39+
const filteredResult = await new GranolaBubble({
40+
operation: 'list_notes',
41+
page_size: 3,
42+
created_after: '2024-01-01',
43+
}).action();
44+
45+
results.push({
46+
operation: 'list_notes (date filter)',
47+
success: filteredResult.success,
48+
details: filteredResult.success
49+
? `Retrieved ${filteredResult.notes?.length ?? 0} notes after 2024-01-01`
50+
: filteredResult.error,
51+
});
52+
53+
// 3. Get a specific note (use the first note from list if available)
54+
const firstNoteId = listResult.success
55+
? listResult.notes?.[0]?.id
56+
: undefined;
57+
const noteId = firstNoteId || '';
58+
59+
if (firstNoteId) {
60+
const getResult = await new GranolaBubble({
61+
operation: 'get_note',
62+
note_id: firstNoteId,
63+
include_transcript: false,
64+
}).action();
65+
66+
results.push({
67+
operation: 'get_note',
68+
success: getResult.success,
69+
details: getResult.success
70+
? `Retrieved note: "${getResult.note?.title}" with ${getResult.note?.attendees?.length ?? 0} attendees`
71+
: getResult.error,
72+
});
73+
74+
// 4. Get same note with transcript
75+
const transcriptResult = await new GranolaBubble({
76+
operation: 'get_note',
77+
note_id: firstNoteId,
78+
include_transcript: true,
79+
}).action();
80+
81+
results.push({
82+
operation: 'get_note (with transcript)',
83+
success: transcriptResult.success,
84+
details: transcriptResult.success
85+
? `Transcript entries: ${transcriptResult.note?.transcript?.length ?? 'null (no transcript)'}`
86+
: transcriptResult.error,
87+
});
88+
} else {
89+
results.push({
90+
operation: 'get_note',
91+
success: false,
92+
details: 'Skipped - no notes available from list_notes',
93+
});
94+
}
95+
96+
// 5. Test pagination with cursor
97+
if (listResult.success && listResult.hasMore && listResult.cursor) {
98+
const paginatedResult = await new GranolaBubble({
99+
operation: 'list_notes',
100+
page_size: 5,
101+
cursor: listResult.cursor,
102+
}).action();
103+
104+
results.push({
105+
operation: 'list_notes (pagination)',
106+
success: paginatedResult.success,
107+
details: paginatedResult.success
108+
? `Page 2: ${paginatedResult.notes?.length ?? 0} notes`
109+
: paginatedResult.error,
110+
});
111+
}
112+
113+
// 6. Test error handling - invalid note ID
114+
const invalidResult = await new GranolaBubble({
115+
operation: 'get_note',
116+
note_id: 'not_INVALID000000',
117+
}).action();
118+
119+
results.push({
120+
operation: 'get_note (invalid ID)',
121+
success: !invalidResult.success, // We expect this to fail
122+
details: !invalidResult.success
123+
? `Correctly returned error: ${invalidResult.error}`
124+
: 'Unexpectedly succeeded with invalid ID',
125+
});
126+
127+
return {
128+
noteId,
129+
testResults: results,
130+
};
131+
}
132+
}

0 commit comments

Comments
 (0)