Skip to content

Commit c6b5e0b

Browse files
committed
Improve callback type safety
1 parent f79175e commit c6b5e0b

24 files changed

Lines changed: 419 additions & 466 deletions

File tree

.changeset/free-meals-switch.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@plotday/twister": minor
3+
"@plotday/tool-outlook-calendar": patch
4+
"@plotday/tool-google-calendar": patch
5+
"@plotday/tool-google-contacts": patch
6+
"@plotday/tool-linear": patch
7+
"@plotday/tool-asana": patch
8+
"@plotday/tool-gmail": patch
9+
"@plotday/tool-slack": patch
10+
"@plotday/tool-jira": patch
11+
---
12+
13+
Changed: Improve callback type safety

tools/asana/src/asana.ts

Lines changed: 28 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import * as asana from "asana";
22

33
import {
4+
type Activity,
45
type ActivityLink,
56
ActivityLinkType,
7+
ActivityMeta,
68
ActivityType,
79
type NewActivityWithNotes,
810
type NewNote,
9-
Uuid,
11+
type Serializable,
1012
} from "@plotday/twister";
1113
import type {
1214
Project,
@@ -74,13 +76,9 @@ export class Asana extends Tool<Asana> implements ProjectTool {
7476
* Request Asana OAuth authorization
7577
*/
7678
async requestAuth<
77-
TCallback extends (auth: ProjectAuth, ...args: any[]) => any
78-
>(
79-
callback: TCallback,
80-
...extraArgs: TCallback extends (auth: any, ...rest: infer R) => any
81-
? R
82-
: []
83-
): Promise<ActivityLink> {
79+
TArgs extends Serializable[],
80+
TCallback extends (auth: ProjectAuth, ...args: TArgs) => any
81+
>(callback: TCallback, ...extraArgs: TArgs): Promise<ActivityLink> {
8482
const asanaScopes = ["default"];
8583

8684
// Generate opaque token for authorization
@@ -153,16 +151,15 @@ export class Asana extends Tool<Asana> implements ProjectTool {
153151
* Start syncing tasks from an Asana project
154152
*/
155153
async startSync<
156-
TCallback extends (task: NewActivityWithNotes, ...args: any[]) => any
154+
TArgs extends Serializable[],
155+
TCallback extends (task: NewActivityWithNotes, ...args: TArgs) => any
157156
>(
158157
options: {
159158
authToken: string;
160159
projectId: string;
161160
} & ProjectSyncOptions,
162161
callback: TCallback,
163-
...extraArgs: TCallback extends (task: any, ...rest: infer R) => any
164-
? R
165-
: []
162+
...extraArgs: TArgs
166163
): Promise<void> {
167164
const { authToken, projectId, timeMin } = options;
168165

@@ -384,7 +381,10 @@ export class Asana extends Tool<Asana> implements ProjectTool {
384381
},
385382
author: authorContact,
386383
assignee: assigneeContact ?? null, // Explicitly set to null for unassigned tasks
387-
done: task.completed && task.completed_at ? new Date(task.completed_at) : null,
384+
done:
385+
task.completed && task.completed_at
386+
? new Date(task.completed_at)
387+
: null,
388388
notes,
389389
};
390390
}
@@ -393,37 +393,29 @@ export class Asana extends Tool<Asana> implements ProjectTool {
393393
* Update task with new values
394394
*
395395
* @param authToken - Authorization token
396-
* @param update - ActivityUpdate with changed fields
396+
* @param activity - The updated activity
397397
*/
398-
async updateIssue(
399-
authToken: string,
400-
update: import("@plotday/twister").ActivityUpdate
401-
): Promise<void> {
398+
async updateIssue(authToken: string, activity: Activity): Promise<void> {
402399
// Extract Asana task GID from meta
403-
const source = update.meta?.source as string | undefined;
404-
const taskGid = source?.split(":").pop();
400+
const taskGid = activity.meta?.taskGid as string | undefined;
405401
if (!taskGid) {
406-
throw new Error("Invalid source format for Asana task");
402+
throw new Error("Asana task GID not found in activity meta");
407403
}
408404

409405
const client = await this.getClient(authToken);
410406
const updateFields: any = {};
411407

412408
// Handle title
413-
if (update.title !== undefined) {
414-
updateFields.name = update.title;
409+
if (activity.title !== null) {
410+
updateFields.name = activity.title;
415411
}
416412

417413
// Handle assignee
418-
if (update.assignee !== undefined) {
419-
updateFields.assignee = update.assignee?.id || null;
420-
}
414+
updateFields.assignee = activity.assignee?.id || null;
421415

422416
// Handle completion status based on done
423417
// Asana only has completed boolean (no In Progress state)
424-
if (update.done !== undefined) {
425-
updateFields.completed = update.done !== null;
426-
}
418+
updateFields.completed = activity.done !== null;
427419

428420
// Apply updates if any fields changed
429421
if (Object.keys(updateFields).length > 0) {
@@ -435,17 +427,21 @@ export class Asana extends Tool<Asana> implements ProjectTool {
435427
* Add a comment (story) to an Asana task
436428
*
437429
* @param authToken - Authorization token
438-
* @param issueId - Asana task GID
430+
* @param meta - Activity metadata containing taskGid
439431
* @param body - Comment text (markdown not directly supported, plain text)
440432
*/
441433
async addIssueComment(
442434
authToken: string,
443-
issueId: string,
435+
meta: ActivityMeta,
444436
body: string
445437
): Promise<void> {
438+
const taskGid = meta.taskGid as string | undefined;
439+
if (!taskGid) {
440+
throw new Error("Asana task GID not found in activity meta");
441+
}
446442
const client = await this.getClient(authToken);
447443

448-
await client.tasks.addComment(issueId, {
444+
await client.tasks.addComment(taskGid, {
449445
text: body,
450446
});
451447
}

tools/gmail/src/gmail.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
type ActivityLink,
3+
Serializable,
34
type SyncUpdate,
45
Tool,
56
type ToolBuilder,
@@ -111,10 +112,9 @@ export class Gmail extends Tool<Gmail> implements MessagingTool {
111112
}
112113

113114
async requestAuth<
114-
TCallback extends (auth: MessagingAuth, ...args: any[]) => any
115-
>(callback: TCallback, ...extraArgs: any[]): Promise<ActivityLink> {
116-
console.log("Requesting Gmail auth");
117-
115+
TArgs extends Serializable[],
116+
TCallback extends (auth: MessagingAuth, ...args: TArgs) => any
117+
>(callback: TCallback, ...extraArgs: TArgs): Promise<ActivityLink> {
118118
// Gmail OAuth scopes for read-only access
119119
const gmailScopes = [
120120
"https://www.googleapis.com/auth/gmail.readonly",
@@ -199,16 +199,15 @@ export class Gmail extends Tool<Gmail> implements MessagingTool {
199199
}
200200

201201
async startSync<
202-
TCallback extends (syncUpdate: SyncUpdate, ...args: any[]) => any
202+
TArgs extends Serializable[],
203+
TCallback extends (syncUpdate: SyncUpdate, ...args: TArgs) => any
203204
>(
204205
options: {
205206
authToken: string;
206207
channelId: string;
207208
} & MessageSyncOptions,
208209
callback: TCallback,
209-
...extraArgs: TCallback extends (syncUpdate: any, ...rest: infer R) => any
210-
? R
211-
: []
210+
...extraArgs: TArgs
212211
): Promise<void> {
213212
const { authToken, channelId, timeMin } = options;
214213
console.log("Starting Gmail sync for channel", channelId);
@@ -237,7 +236,6 @@ export class Gmail extends Tool<Gmail> implements MessagingTool {
237236

238237
await this.set(`sync_state_${channelId}`, initialState);
239238

240-
console.log("Starting initial Gmail sync");
241239
// Start sync batch using run tool for long-running operation
242240
const syncCallback = await this.callback(
243241
this.syncBatch,

tools/google-calendar/src/google-calendar.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ import {
33
type Activity,
44
type ActivityLink,
55
ActivityLinkType,
6-
ActivityUpdate,
76
type ActorId,
87
ConferencingProvider,
98
type NewActivityWithNotes,
109
type NewActor,
1110
type NewContact,
1211
type NewNote,
12+
Serializable,
1313
Tag,
1414
Tool,
1515
type ToolBuilder,
@@ -140,8 +140,9 @@ export class GoogleCalendar
140140
}
141141

142142
async requestAuth<
143-
TCallback extends (auth: CalendarAuth, ...args: any[]) => any
144-
>(callback: TCallback, ...extraArgs: any[]): Promise<ActivityLink> {
143+
TArgs extends Serializable[],
144+
TCallback extends (auth: CalendarAuth, ...args: TArgs) => any
145+
>(callback: TCallback, ...extraArgs: TArgs): Promise<ActivityLink> {
145146
console.log("Requesting Google Calendar auth");
146147

147148
// Combine calendar and contacts scopes for single OAuth flow
@@ -238,14 +239,15 @@ export class GoogleCalendar
238239
}
239240

240241
async startSync<
241-
TCallback extends (activity: NewActivityWithNotes, ...args: any[]) => any
242+
TArgs extends Serializable[],
243+
TCallback extends (activity: NewActivityWithNotes, ...args: TArgs) => any
242244
>(
243245
options: {
244246
authToken: string;
245247
calendarId: string;
246248
} & SyncOptions,
247249
callback: TCallback,
248-
...extraArgs: any[]
250+
...extraArgs: TArgs
249251
): Promise<void> {
250252
const { authToken, calendarId } = options;
251253
console.log("Saving callback");
@@ -836,14 +838,12 @@ export class GoogleCalendar
836838
async onActivityUpdated(
837839
activity: Activity,
838840
changes: {
839-
update: ActivityUpdate;
840-
previous: Activity;
841841
tagsAdded: Record<Tag, ActorId[]>;
842842
tagsRemoved: Record<Tag, ActorId[]>;
843843
}
844844
): Promise<void> {
845845
// Only process calendar events
846-
const source = activity.meta?.source;
846+
const source = activity.source;
847847
if (
848848
!source ||
849849
typeof source !== "string" ||

tools/google-contacts/src/google-contacts.ts

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { Tool, type ToolBuilder, type NewContact } from "@plotday/twister";
1+
import {
2+
type NewContact,
3+
Serializable,
4+
Tool,
5+
type ToolBuilder,
6+
} from "@plotday/twister";
27
import { type Callback } from "@plotday/twister/tools/callbacks";
38
import {
49
AuthLevel,
@@ -9,10 +14,7 @@ import {
914
} from "@plotday/twister/tools/integrations";
1015
import { Network } from "@plotday/twister/tools/network";
1116

12-
import type {
13-
ContactAuth,
14-
GoogleContacts as IGoogleContacts,
15-
} from "./types";
17+
import type { ContactAuth, GoogleContacts as IGoogleContacts } from "./types";
1618

1719
type ContactTokens = {
1820
connections?: {
@@ -269,8 +271,9 @@ export default class GoogleContacts
269271
}
270272

271273
async requestAuth<
272-
TCallback extends (auth: ContactAuth, ...args: any[]) => any
273-
>(callback: TCallback, ...extraArgs: any[]): Promise<any> {
274+
TArgs extends Serializable[],
275+
TCallback extends (auth: ContactAuth, ...args: TArgs) => any
276+
>(callback: TCallback, ...extraArgs: TArgs): Promise<any> {
274277
const opaqueToken = crypto.randomUUID();
275278

276279
// Create callback token for parent
@@ -311,12 +314,9 @@ export default class GoogleContacts
311314
}
312315

313316
async startSync<
314-
TCallback extends (contacts: NewContact[], ...args: any[]) => any
315-
>(
316-
authToken: string,
317-
callback: TCallback,
318-
...extraArgs: any[]
319-
): Promise<void> {
317+
TArgs extends Serializable[],
318+
TCallback extends (contacts: NewContact[], ...args: TArgs) => any
319+
>(authToken: string, callback: TCallback, ...extraArgs: TArgs): Promise<void> {
320320
const storedAuthToken = await this.get<AuthToken>(
321321
`auth_token:${authToken}`
322322
);
@@ -356,12 +356,13 @@ export default class GoogleContacts
356356
* @param extraArgs - Additional arguments to pass to the callback
357357
*/
358358
async syncWithAuth<
359-
TCallback extends (contacts: NewContact[], ...args: any[]) => any
359+
TArgs extends Serializable[],
360+
TCallback extends (contacts: NewContact[], ...args: TArgs) => any
360361
>(
361362
authorization: Authorization,
362363
authToken: AuthToken,
363364
callback?: TCallback,
364-
...extraArgs: any[]
365+
...extraArgs: TArgs
365366
): Promise<void> {
366367
// Validate authorization has required contacts scopes
367368
const hasRequiredScopes = GoogleContacts.SCOPES.every((scope) =>

0 commit comments

Comments
 (0)