Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
95 changes: 95 additions & 0 deletions src/lib/api/notifications.test.ts
Original file line number Diff line number Diff line change
@@ -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: '*',
})
})
})
29 changes: 13 additions & 16 deletions src/lib/api/notifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>

let fromUser: NotificationUser | undefined
if (n.fromUid) {
const fromUserData = raw.from_user as Record<string, unknown> | 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,
}
}

Expand Down
Loading