From 8ea820376d356f351e155a61e91f6baa5b646181 Mon Sep 17 00:00:00 2001 From: Scott Lovegrove Date: Tue, 3 Dec 2024 09:31:17 +0000 Subject: [PATCH 1/2] chore: Switch to a different completed tasks endpoint --- src/services/todoist.service.ts | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/services/todoist.service.ts b/src/services/todoist.service.ts index 95c0570..da2fab4 100644 --- a/src/services/todoist.service.ts +++ b/src/services/todoist.service.ts @@ -4,7 +4,7 @@ import { lastValueFrom } from 'rxjs' import type { Task } from '../types' -const LIMIT = 100 +const LIMIT = 200 type SyncDue = { date: string @@ -36,6 +36,12 @@ export type SyncTask = { due?: SyncDue | null } +type CompletedTasksResponse = { + items: SyncTask[] + total: number + next_cursor?: string +} + @Injectable() export class TodoistService { constructor(private readonly httpService: HttpService) {} @@ -47,25 +53,29 @@ export class TodoistService { token: string projectId: string }): Promise { - const completedTasks = await this.getCompletedTasksInternal({ token, offset: 0, projectId }) + const completedTasks = await this.getCompletedTasksInternal({ token, projectId }) return completedTasks.map((task) => this.getTaskFromQuickAddResponse(task)) } private async getCompletedTasksInternal({ - offset, + cursor, projectId, token, }: { token: string - offset: number + cursor?: string projectId: string }): Promise { const response = await lastValueFrom( - this.httpService.get( - // At time of writing (08/02/2023), this endpoint is undocumented and its stability is not guaranteed. - `https://api.todoist.com/sync/v9/items/get_completed?project_id=${projectId}&offset=${offset}&limit=${LIMIT}`, + this.httpService.get( + 'https://api.todoist.com/sync/v9/tasks/archived', { + params: { + project_id: projectId, + cursor, + limit: LIMIT, + }, headers: { Authorization: `Bearer ${token}`, }, @@ -73,11 +83,11 @@ export class TodoistService { ), ) - const { data: tasks } = response + const { items: tasks, next_cursor } = response.data if (tasks.length === LIMIT) { return tasks.concat( - await this.getCompletedTasksInternal({ token, offset: offset + LIMIT, projectId }), + await this.getCompletedTasksInternal({ token, cursor: next_cursor, projectId }), ) } From 2f42f2f3866e2947785ba8a33b03f2cb1ecbea72 Mon Sep 17 00:00:00 2001 From: Scott Lovegrove Date: Tue, 3 Dec 2024 10:46:57 +0000 Subject: [PATCH 2/2] test: Fix the tests --- src/services/todoist.service.spec.ts | 44 +++++++++++++++------------- src/services/todoist.service.ts | 2 +- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/src/services/todoist.service.spec.ts b/src/services/todoist.service.spec.ts index 0a2a712..e6efd52 100644 --- a/src/services/todoist.service.spec.ts +++ b/src/services/todoist.service.spec.ts @@ -1,6 +1,5 @@ import { HttpModule } from '@nestjs/axios' import { Test } from '@nestjs/testing' -import { chunk } from 'lodash' import { rest } from 'msw' import { server } from '../../test/server' @@ -37,17 +36,18 @@ describe('TodoistService', () => { expect(httpServer).toHaveBeenCalledTimes(1) }) - test.each([ - [0, 1], - [50, 1], - [99, 1], - [100, 2], - [101, 2], - [150, 2], - [199, 2], - [200, 3], - ])('when task count is %i, should call %i times', async (taskCount, expectedCalls) => { - setupGetCompletedItems(Array(taskCount).fill({}) as SyncTask[]) + type TestCase = [number, number, string | undefined] + test.each([ + [0, 1, ''], + [50, 1, ''], + [99, 1, ''], + [100, 1, ''], + [101, 1, ''], + [150, 1, ''], + [199, 1, ''], + [200, 2, 'cursor'], + ])('when task count is %i, should call %i times', async (taskCount, expectedCalls, cursor) => { + setupGetCompletedItems(Array(taskCount).fill({}) as SyncTask[], cursor) const target = await getTarget() const httpServer = jest.spyOn(target['httpService'], 'get') @@ -58,20 +58,22 @@ describe('TodoistService', () => { expect(httpServer).toHaveBeenCalledTimes(expectedCalls) }) - function setupGetCompletedItems(items: SyncTask[]) { - const allTasks = chunk(items, 100) - + function setupGetCompletedItems(items: SyncTask[], cursor?: string) { server.use( - rest.get('https://api.todoist.com/sync/v9/items/get_completed', (req, res, ctx) => { - const offset = getOffset(req.url) - const tasks = allTasks[offset / 100] ?? [] - return res(ctx.json(tasks)) + rest.get('https://api.todoist.com/sync/v9/tasks/archived', (req, res, ctx) => { + const requestedCursor = getCursor(req.url) + return res( + ctx.json({ + ...(cursor && !requestedCursor ? { next_cursor: cursor } : {}), + items, + }), + ) }), ) } - function getOffset(url: URL) { - return parseInt(url.searchParams.get('offset') || '0', 10) + function getCursor(url: URL) { + return url.searchParams.get('cursor') } async function getTarget() { diff --git a/src/services/todoist.service.ts b/src/services/todoist.service.ts index da2fab4..9a03eb2 100644 --- a/src/services/todoist.service.ts +++ b/src/services/todoist.service.ts @@ -85,7 +85,7 @@ export class TodoistService { const { items: tasks, next_cursor } = response.data - if (tasks.length === LIMIT) { + if (next_cursor) { return tasks.concat( await this.getCompletedTasksInternal({ token, cursor: next_cursor, projectId }), )