Skip to content

Commit 8053f7a

Browse files
committed
Simpler upsert-based sync
1 parent a8840e4 commit 8053f7a

File tree

30 files changed

+1504
-841
lines changed

30 files changed

+1504
-841
lines changed

.changeset/cute-showers-scream.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@plotday/tool-outlook-calendar": minor
3+
"@plotday/tool-google-calendar": minor
4+
"@plotday/tool-linear": minor
5+
"@plotday/tool-asana": minor
6+
"@plotday/tool-gmail": minor
7+
"@plotday/tool-slack": minor
8+
"@plotday/tool-jira": minor
9+
---
10+
11+
Changed: Use Activity.source with canonical URLs for upserting

.changeset/petite-bats-think.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@plotday/twister": minor
3+
---
4+
5+
Added: Activity.source and Note.key for upserts

tools/asana/src/asana.ts

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ import * as asana from "asana";
22

33
import {
44
type ActivityLink,
5+
ActivityLinkType,
56
ActivityType,
67
type NewActivityWithNotes,
8+
type NewNote,
9+
Uuid,
710
} from "@plotday/twister";
811
import type {
912
Project,
@@ -269,8 +272,8 @@ export class Asana extends Tool<Asana> implements ProjectTool {
269272
for (const task of tasksResult.data) {
270273
// Optionally filter by time
271274
if (options?.timeMin) {
272-
const createdAt = new Date(task.created_at);
273-
if (createdAt < options.timeMin) {
275+
const created = new Date(task.created_at);
276+
if (created < options.timeMin) {
274277
continue;
275278
}
276279
}
@@ -339,28 +342,46 @@ export class Asana extends Tool<Asana> implements ProjectTool {
339342
};
340343
}
341344

342-
// Build notes array: description
343-
const notes: Array<{ content: string }> = [];
345+
// Build notes array: always create initial note with description and link
346+
const notes: NewNote[] = [];
344347

348+
// Extract description (if any)
349+
let description: string | null = null;
345350
if (task.notes) {
346-
notes.push({ content: task.notes });
351+
description = task.notes;
347352
}
348353

349-
// Ensure at least one note exists
350-
if (notes.length === 0) {
351-
notes.push({ content: "" });
352-
}
354+
// Construct Asana task URL
355+
const taskUrl = `https://app.asana.com/0/${projectId}/${task.gid}`;
356+
357+
// Create initial note with description and link to Asana task
358+
const links: ActivityLink[] = [];
359+
links.push({
360+
type: ActivityLinkType.external,
361+
title: `Open in Asana`,
362+
url: taskUrl,
363+
});
364+
365+
notes.push({
366+
activity: { source: taskUrl },
367+
key: "description",
368+
content: description,
369+
created: task.created_at ? new Date(task.created_at) : undefined,
370+
links: links.length > 0 ? links : null,
371+
});
353372

354373
return {
374+
source: taskUrl,
355375
type: ActivityType.Action,
356376
title: task.name,
377+
created: task.created_at ? new Date(task.created_at) : undefined,
357378
meta: {
358-
source: `asana:task:${projectId}:${task.gid}`,
359379
taskGid: task.gid,
380+
projectId,
360381
},
361382
author: authorContact,
362-
assignee: assigneeContact,
363-
doneAt: task.completed ? new Date(task.completed_at || Date.now()) : null,
383+
assignee: assigneeContact ?? null, // Explicitly set to null for unassigned tasks
384+
done: task.completed && task.completed_at ? new Date(task.completed_at) : null,
364385
notes,
365386
};
366387
}
@@ -395,10 +416,10 @@ export class Asana extends Tool<Asana> implements ProjectTool {
395416
updateFields.assignee = update.assignee?.id || null;
396417
}
397418

398-
// Handle completion status based on doneAt
419+
// Handle completion status based on done
399420
// Asana only has completed boolean (no In Progress state)
400-
if (update.doneAt !== undefined) {
401-
updateFields.completed = update.doneAt !== null;
421+
if (update.done !== undefined) {
422+
updateFields.completed = update.done !== null;
402423
}
403424

404425
// Apply updates if any fields changed

tools/gmail/src/gmail-api.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import type {
33
NewActivityWithNotes,
44
NewActor,
55
} from "@plotday/twister/plot";
6-
import { Uuid } from "@plotday/twister/utils/uuid";
76

87
export type GmailLabel = {
98
id: string;
@@ -360,15 +359,18 @@ export function transformGmailThread(thread: GmailThread): NewActivityWithNotes
360359
const parentMessage = thread.messages[0];
361360
const subject = getHeader(parentMessage, "Subject");
362361

362+
// Canonical URL for the thread
363+
const canonicalUrl = `https://mail.google.com/mail/u/0/#inbox/${thread.id}`;
364+
363365
// Create Activity
364366
const activity: NewActivityWithNotes = {
367+
source: canonicalUrl,
365368
type: ActivityType.Note,
366369
title: subject || "Email",
367370
start: new Date(parseInt(parentMessage.internalDate)),
368371
meta: {
369372
threadId: thread.id,
370373
historyId: thread.historyId,
371-
source: `gmail:${thread.id}`,
372374
},
373375
notes: [],
374376
};
@@ -390,9 +392,10 @@ export function transformGmailThread(thread: GmailThread): NewActivityWithNotes
390392
...parseEmailAddressesToNewActors(cc),
391393
];
392394

393-
// Create NewNote (Omit<NewNote, "activity">)
395+
// Create NewNote with idempotent key
394396
const note = {
395-
id: Uuid.Generate(),
397+
activity: { source: canonicalUrl },
398+
key: message.id,
396399
author: {
397400
email: sender.email,
398401
name: sender.name || undefined,

tools/gmail/src/gmail.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ export class Gmail extends Tool<Gmail> implements MessagingTool {
305305
channelId,
306306
historyId: watchResult.historyId,
307307
expiration: new Date(parseInt(watchResult.expiration)),
308-
createdAt: new Date().toISOString(),
308+
created: new Date().toISOString(),
309309
});
310310

311311
console.log("Gmail webhook setup complete", {

0 commit comments

Comments
 (0)