diff --git a/package-lock.json b/package-lock.json index 289fa56..c46e937 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "license": "MIT", "dependencies": { "@doist/cli-core": "1.1.0", - "@doist/todoist-sdk": "14.0.1", + "@doist/todoist-sdk": "14.0.2", "@napi-rs/keyring": "1.3.0", "@pnpm/tabtab": "0.5.4", "chalk": "6.0.0", @@ -204,9 +204,9 @@ } }, "node_modules/@doist/todoist-sdk": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/@doist/todoist-sdk/-/todoist-sdk-14.0.1.tgz", - "integrity": "sha512-l2mXCZHFkKWHyVeqGj9dZONAyOLCP5Qy89msUH4/45SDfubpFN+lx6D5Q52w2Qkc8sy6nV8rnTR7E6U7jS9myg==", + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/@doist/todoist-sdk/-/todoist-sdk-14.0.2.tgz", + "integrity": "sha512-ppV2TUT8QDsiwC896pUxqTwdflSU/C8R3Vutv1ef4RAmY3Ch+0RlueZKOf+TGkwIGi88iWw4cxB+Ti1G3pAG0w==", "license": "MIT", "dependencies": { "camelcase": "6.3.0", diff --git a/package.json b/package.json index f7dc6e7..42fc029 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ ], "dependencies": { "@doist/cli-core": "1.1.0", - "@doist/todoist-sdk": "14.0.1", + "@doist/todoist-sdk": "14.0.2", "@napi-rs/keyring": "1.3.0", "@pnpm/tabtab": "0.5.4", "chalk": "6.0.0", diff --git a/src/lib/api/notifications.test.ts b/src/lib/api/notifications.test.ts new file mode 100644 index 0000000..4123af0 --- /dev/null +++ b/src/lib/api/notifications.test.ts @@ -0,0 +1,95 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +vi.mock('./core.js', () => ({ + getApi: vi.fn(), +})) + +import type { LiveNotification } from '@doist/todoist-sdk' +import { setupApiMock } from '../../test-support/api-mock.js' +import type { MockApi } from '../../test-support/mock-api.js' +import { fetchNotifications } from './notifications.js' + +describe('fetchNotifications', () => { + let mockApi: MockApi + + beforeEach(() => { + vi.clearAllMocks() + mockApi = setupApiMock() + }) + + it('maps SDK notification fields, filters deleted entries, and sorts newest first', async () => { + const notifications: LiveNotification[] = [ + { + id: 'older', + createdAt: new Date('2026-08-17T10:00:00Z'), + fromUid: 'user-1', + notificationType: 'item_assigned', + isUnread: false, + projectId: 'project-1', + projectName: 'Work', + itemId: 'task-1', + itemContent: 'Review the proposal', + }, + { + id: 'newer', + createdAt: new Date('2026-08-18T10:00:00Z'), + notificationType: 'share_invitation_sent', + isUnread: true, + fromUser: { + id: 'user-2', + fullName: 'Jane Doe', + email: 'jane@example.com', + imageId: null, + }, + projectId: 'project-2', + projectName: 'Shared project', + invitationId: 'invitation-1', + invitationSecret: 'secret-1', + }, + { + id: 'deleted', + createdAt: new Date('2026-08-19T10:00:00Z'), + fromUid: 'user-3', + notificationType: 'project_archived', + isUnread: true, + isDeleted: true, + }, + ] + mockApi.sync.mockResolvedValue({ liveNotifications: notifications }) + + await expect(fetchNotifications()).resolves.toStrictEqual([ + { + id: 'newer', + type: 'share_invitation_sent', + isUnread: true, + isDeleted: false, + createdAt: new Date('2026-08-18T10:00:00Z'), + fromUser: { + id: 'user-2', + name: 'Jane Doe', + email: 'jane@example.com', + }, + project: { id: 'project-2', name: 'Shared project' }, + task: undefined, + invitationId: 'invitation-1', + invitationSecret: 'secret-1', + }, + { + id: 'older', + type: 'item_assigned', + isUnread: false, + isDeleted: false, + createdAt: new Date('2026-08-17T10:00:00Z'), + fromUser: { id: 'user-1', name: '', email: '' }, + project: { id: 'project-1', name: 'Work' }, + task: { id: 'task-1', content: 'Review the proposal' }, + invitationId: undefined, + invitationSecret: undefined, + }, + ]) + expect(mockApi.sync).toHaveBeenCalledWith({ + resourceTypes: ['live_notifications'], + syncToken: '*', + }) + }) +}) diff --git a/src/lib/api/notifications.ts b/src/lib/api/notifications.ts index f2d06ee..544a300 100644 --- a/src/lib/api/notifications.ts +++ b/src/lib/api/notifications.ts @@ -50,46 +50,43 @@ export interface Notification { } function parseNotification(n: LiveNotification): Notification { - // The SDK type uses passthrough() so extra fields are preserved - const raw = n as Record - let fromUser: NotificationUser | undefined - if (n.fromUid) { - const fromUserData = raw.from_user as Record | undefined + const fromUserId = n.fromUser?.id ?? n.fromUid + if (fromUserId) { fromUser = { - id: String(n.fromUid), - name: String(fromUserData?.full_name ?? fromUserData?.name ?? ''), - email: String(fromUserData?.email ?? ''), + id: fromUserId, + name: n.fromUser?.fullName ?? '', + email: n.fromUser?.email ?? '', } } let project: NotificationProject | undefined if (n.projectId) { project = { - id: String(n.projectId), - name: String(raw.project_name ?? ''), + id: n.projectId, + name: n.projectName ?? '', } } let task: NotificationTask | undefined if (n.itemId) { task = { - id: String(n.itemId), - content: String(n.itemContent ?? ''), + id: n.itemId, + content: n.itemContent ?? '', } } return { - id: String(n.id), + id: n.id, type: n.notificationType as NotificationType, isUnread: n.isUnread, - isDeleted: Boolean(raw.is_deleted ?? false), + isDeleted: n.isDeleted ?? false, createdAt: n.createdAt, fromUser, project, task, - invitationId: n.invitationId ? String(n.invitationId) : undefined, - invitationSecret: raw.invitation_secret ? String(raw.invitation_secret) : undefined, + invitationId: n.invitationId, + invitationSecret: n.invitationSecret, } }