diff --git a/src/TodoistApi.tasks.test.ts b/src/TodoistApi.tasks.test.ts index 328645f..4295a2d 100644 --- a/src/TodoistApi.tasks.test.ts +++ b/src/TodoistApi.tasks.test.ts @@ -12,6 +12,8 @@ import { ENDPOINT_REST_TASK_REOPEN, ENDPOINT_REST_TASKS, ENDPOINT_REST_TASKS_FILTER, + ENDPOINT_REST_TASKS_COMPLETED_BY_COMPLETION_DATE, + ENDPOINT_REST_TASKS_COMPLETED_BY_DUE_DATE, ENDPOINT_SYNC_QUICK_ADD, } from './consts/endpoints' import { setupRestClientMock } from './testUtils/mocks' @@ -330,4 +332,102 @@ describe('TodoistApi task endpoints', () => { await expect(api.getTasksByFilter(DEFAULT_GET_TASKS_BY_FILTER_ARGS)).rejects.toThrow() }) }) + + describe('getCompletedTasksByCompletionDate', () => { + const DEFAULT_GET_COMPLETED_TASKS_ARGS = { + since: '2025-01-01T00:00:00Z', + until: '2025-12-31T23:59:59Z', + workspaceId: null, + cursor: null, + limit: 10, + } + + test('calls get request with expected url', async () => { + const requestMock = setupRestClientMock({ items: [DEFAULT_TASK], nextCursor: null }) + const api = getTarget() + + await api.getCompletedTasksByCompletionDate(DEFAULT_GET_COMPLETED_TASKS_ARGS) + + expect(requestMock).toBeCalledTimes(1) + expect(requestMock).toBeCalledWith( + 'GET', + getSyncBaseUri(), + ENDPOINT_REST_TASKS_COMPLETED_BY_COMPLETION_DATE, + DEFAULT_AUTH_TOKEN, + DEFAULT_GET_COMPLETED_TASKS_ARGS, + ) + }) + + test('returns result from rest client', async () => { + setupRestClientMock({ items: [DEFAULT_TASK], nextCursor: '123' }) + const api = getTarget() + + const response = await api.getCompletedTasksByCompletionDate( + DEFAULT_GET_COMPLETED_TASKS_ARGS, + ) + + expect(response).toEqual({ + items: [DEFAULT_TASK], + nextCursor: '123', + }) + }) + + test('validates task array in response', async () => { + const invalidTask = { ...DEFAULT_TASK, due: '2020-01-31' } + setupRestClientMock({ items: [invalidTask], nextCursor: null }) + const api = getTarget() + + await expect( + api.getCompletedTasksByCompletionDate(DEFAULT_GET_COMPLETED_TASKS_ARGS), + ).rejects.toThrow() + }) + }) + + describe('getCompletedTasksByDueDate', () => { + const DEFAULT_GET_COMPLETED_TASKS_ARGS = { + since: '2025-01-01T00:00:00Z', + until: '2025-12-31T23:59:59Z', + workspaceId: null, + cursor: null, + limit: 10, + } + + test('calls get request with expected url', async () => { + const requestMock = setupRestClientMock({ items: [DEFAULT_TASK], nextCursor: null }) + const api = getTarget() + + await api.getCompletedTasksByDueDate(DEFAULT_GET_COMPLETED_TASKS_ARGS) + + expect(requestMock).toBeCalledTimes(1) + expect(requestMock).toBeCalledWith( + 'GET', + getSyncBaseUri(), + ENDPOINT_REST_TASKS_COMPLETED_BY_DUE_DATE, + DEFAULT_AUTH_TOKEN, + DEFAULT_GET_COMPLETED_TASKS_ARGS, + ) + }) + + test('returns result from rest client', async () => { + setupRestClientMock({ items: [DEFAULT_TASK], nextCursor: '456' }) + const api = getTarget() + + const response = await api.getCompletedTasksByDueDate(DEFAULT_GET_COMPLETED_TASKS_ARGS) + + expect(response).toEqual({ + items: [DEFAULT_TASK], + nextCursor: '456', + }) + }) + + test('validates task array in response', async () => { + const invalidTask = { ...DEFAULT_TASK, due: '2020-01-31' } + setupRestClientMock({ items: [invalidTask], nextCursor: null }) + const api = getTarget() + + await expect( + api.getCompletedTasksByDueDate(DEFAULT_GET_COMPLETED_TASKS_ARGS), + ).rejects.toThrow() + }) + }) }) diff --git a/src/TodoistApi.ts b/src/TodoistApi.ts index b4ce8fc..5362411 100644 --- a/src/TodoistApi.ts +++ b/src/TodoistApi.ts @@ -30,12 +30,17 @@ import { GetSharedLabelsResponse, GetCommentsResponse, type MoveTaskArgs, + GetCompletedTasksByCompletionDateArgs, + GetCompletedTasksByDueDateArgs, + GetCompletedTasksResponse, } from './types/requests' import { request, isSuccess } from './restClient' import { getSyncBaseUri, ENDPOINT_REST_TASKS, ENDPOINT_REST_TASKS_FILTER, + ENDPOINT_REST_TASKS_COMPLETED_BY_COMPLETION_DATE, + ENDPOINT_REST_TASKS_COMPLETED_BY_DUE_DATE, ENDPOINT_REST_PROJECTS, ENDPOINT_SYNC_QUICK_ADD, ENDPOINT_REST_TASK_CLOSE, @@ -182,6 +187,56 @@ export class TodoistApi { } } + /** + * Retrieves completed tasks by completion date. + * + * @param args - Parameters for filtering, including required since, until. + * @returns A promise that resolves to a paginated response of completed tasks. + */ + async getCompletedTasksByCompletionDate( + args: GetCompletedTasksByCompletionDateArgs, + ): Promise { + const { + data: { items, nextCursor }, + } = await request( + 'GET', + this.syncApiBase, + ENDPOINT_REST_TASKS_COMPLETED_BY_COMPLETION_DATE, + this.authToken, + args, + ) + + return { + items: validateTaskArray(items), + nextCursor, + } + } + + /** + * Retrieves completed tasks by due date. + * + * @param args - Parameters for filtering, including required since, until. + * @returns A promise that resolves to a paginated response of completed tasks. + */ + async getCompletedTasksByDueDate( + args: GetCompletedTasksByDueDateArgs, + ): Promise { + const { + data: { items, nextCursor }, + } = await request( + 'GET', + this.syncApiBase, + ENDPOINT_REST_TASKS_COMPLETED_BY_DUE_DATE, + this.authToken, + args, + ) + + return { + items: validateTaskArray(items), + nextCursor, + } + } + /** * Creates a new task with the provided parameters. * diff --git a/src/consts/endpoints.ts b/src/consts/endpoints.ts index 6b56992..4b07770 100644 --- a/src/consts/endpoints.ts +++ b/src/consts/endpoints.ts @@ -19,6 +19,10 @@ export function getAuthBaseUri(domainBase: string = TODOIST_URI): string { export const ENDPOINT_REST_TASKS = 'tasks' export const ENDPOINT_REST_TASKS_FILTER = ENDPOINT_REST_TASKS + '/filter' +export const ENDPOINT_REST_TASKS_COMPLETED_BY_COMPLETION_DATE = + ENDPOINT_REST_TASKS + '/completed/by_completion_date' +export const ENDPOINT_REST_TASKS_COMPLETED_BY_DUE_DATE = + ENDPOINT_REST_TASKS + '/completed/by_due_date' export const ENDPOINT_REST_PROJECTS = 'projects' export const ENDPOINT_REST_SECTIONS = 'sections' export const ENDPOINT_REST_LABELS = 'labels' diff --git a/src/types/requests.ts b/src/types/requests.ts index f14a696..a0bf64a 100644 --- a/src/types/requests.ts +++ b/src/types/requests.ts @@ -63,6 +63,41 @@ export type GetTasksByFilterArgs = { limit?: number } +/** + * Arguments for retrieving completed tasks by completion date. + * @see https://todoist.com/api/v1/docs#tag/Tasks/operation/tasks_completed_by_completion_date_api_v1_tasks_completed_by_completion_date_get + */ +export type GetCompletedTasksByCompletionDateArgs = { + since: string + until: string + workspaceId?: string | null + projectId?: string | null + sectionId?: string | null + parentId?: string | null + filterQuery?: string | null + filterLang?: string | null + cursor?: string | null + limit?: number + publicKey?: string | null +} + +/** + * Arguments for retrieving completed tasks by due date. + * @see https://todoist.com/api/v1/docs#tag/Tasks/operation/tasks_completed_by_due_date_api_v1_tasks_completed_by_due_date_get + */ +export type GetCompletedTasksByDueDateArgs = { + since: string + until: string + workspaceId?: string | null + projectId?: string | null + sectionId?: string | null + parentId?: string | null + filterQuery?: string | null + filterLang?: string | null + cursor?: string | null + limit?: number +} + /** * @see https://todoist.com/api/v1/docs#tag/Tasks/operation/get_tasks_api_v1_tasks_get */ @@ -71,6 +106,15 @@ export type GetTasksResponse = { nextCursor: string | null } +/** + * @see https://todoist.com/api/v1/docs#tag/Tasks/operation/tasks_completed_by_due_date_api_v1_tasks_completed_by_due_date_get + * @see https://todoist.com/api/v1/docs#tag/Tasks/operation/tasks_completed_by_completion_date_api_v1_tasks_completed_by_completion_date_get + */ +export type GetCompletedTasksResponse = { + items: Task[] + nextCursor: string | null +} + /** * Arguments for updating a task. * @see https://todoist.com/api/v1/docs#tag/Tasks/operation/update_task_api_v1_tasks__task_id__post