diff --git a/src/TodoistApi.comments.test.ts b/src/TodoistApi.comments.test.ts index 8263ada..12693dc 100644 --- a/src/TodoistApi.comments.test.ts +++ b/src/TodoistApi.comments.test.ts @@ -5,7 +5,11 @@ import { COMMENT_WITH_OPTIONALS_AS_NULL_TASK, DEFAULT_AUTH_TOKEN, DEFAULT_COMMENT, + DEFAULT_RAW_COMMENT, DEFAULT_REQUEST_ID, + RAW_COMMENT_WITH_ATTACHMENT_WITH_OPTIONALS_AS_NULL, + RAW_COMMENT_WITH_OPTIONALS_AS_NULL_PROJECT, + RAW_COMMENT_WITH_OPTIONALS_AS_NULL_TASK, } from './testUtils/testDefaults' import { getSyncBaseUri, ENDPOINT_REST_COMMENTS } from './consts/endpoints' import { setupRestClientMock } from './testUtils/mocks' @@ -19,7 +23,7 @@ describe('TodoistApi comment endpoints', () => { test('calls get request with expected params', async () => { const getCommentsArgs = { projectId: '12', limit: 10, cursor: '0' } const requestMock = setupRestClientMock({ - results: [DEFAULT_COMMENT], + results: [DEFAULT_RAW_COMMENT], nextCursor: '123', }) const api = getTarget() @@ -37,13 +41,19 @@ describe('TodoistApi comment endpoints', () => { }) test('returns result from rest client', async () => { + const mockResponses = [ + DEFAULT_RAW_COMMENT, + RAW_COMMENT_WITH_OPTIONALS_AS_NULL_TASK, + RAW_COMMENT_WITH_OPTIONALS_AS_NULL_PROJECT, + RAW_COMMENT_WITH_ATTACHMENT_WITH_OPTIONALS_AS_NULL, + ] const expectedComments = [ DEFAULT_COMMENT, COMMENT_WITH_OPTIONALS_AS_NULL_TASK, COMMENT_WITH_OPTIONALS_AS_NULL_PROJECT, COMMENT_WITH_ATTACHMENT_WITH_OPTIONALS_AS_NULL, ] - setupRestClientMock({ results: expectedComments, nextCursor: '123' }) + setupRestClientMock({ results: mockResponses, nextCursor: '123' }) const api = getTarget() const { results: comments, nextCursor } = await api.getComments({ taskId: '12' }) @@ -56,7 +66,7 @@ describe('TodoistApi comment endpoints', () => { describe('getComment', () => { test('calls get on expected url', async () => { const commentId = '1' - const requestMock = setupRestClientMock(DEFAULT_COMMENT) + const requestMock = setupRestClientMock(DEFAULT_RAW_COMMENT) const api = getTarget() await api.getComment(commentId) @@ -71,13 +81,12 @@ describe('TodoistApi comment endpoints', () => { }) test('returns result from rest client', async () => { - const expectedComment = DEFAULT_COMMENT - setupRestClientMock(expectedComment) + setupRestClientMock(DEFAULT_RAW_COMMENT) const api = getTarget() const comment = await api.getComment('1') - expect(comment).toEqual(expectedComment) + expect(comment).toEqual(DEFAULT_COMMENT) }) }) @@ -88,7 +97,7 @@ describe('TodoistApi comment endpoints', () => { } test('makes post request with expected params', async () => { - const requestMock = setupRestClientMock(DEFAULT_COMMENT) + const requestMock = setupRestClientMock(DEFAULT_RAW_COMMENT) const api = getTarget() await api.addComment(addCommentArgs, DEFAULT_REQUEST_ID) @@ -105,13 +114,12 @@ describe('TodoistApi comment endpoints', () => { }) test('returns result from rest client', async () => { - const expectedComment = DEFAULT_COMMENT - setupRestClientMock(expectedComment) + setupRestClientMock(DEFAULT_RAW_COMMENT) const api = getTarget() const comment = await api.addComment(addCommentArgs) - expect(comment).toEqual(expectedComment) + expect(comment).toEqual(DEFAULT_COMMENT) }) }) @@ -122,7 +130,7 @@ describe('TodoistApi comment endpoints', () => { test('makes post request with expected params', async () => { const taskId = '1' - const requestMock = setupRestClientMock(DEFAULT_COMMENT, 204) + const requestMock = setupRestClientMock(DEFAULT_RAW_COMMENT, 204) const api = getTarget() await api.updateComment(taskId, updateCommentArgs, DEFAULT_REQUEST_ID) @@ -139,13 +147,14 @@ describe('TodoistApi comment endpoints', () => { }) test('returns success result from rest client', async () => { - const returnedComment = { ...DEFAULT_COMMENT, ...updateCommentArgs } + const returnedComment = { ...DEFAULT_RAW_COMMENT, content: updateCommentArgs.content } + const expectedComment = { ...DEFAULT_COMMENT, content: updateCommentArgs.content } setupRestClientMock(returnedComment, 204) const api = getTarget() const result = await api.updateComment('1', updateCommentArgs) - expect(result).toEqual(returnedComment) + expect(result).toEqual(expectedComment) }) }) diff --git a/src/testUtils/testDefaults.ts b/src/testUtils/testDefaults.ts index 95bda9f..bdec943 100644 --- a/src/testUtils/testDefaults.ts +++ b/src/testUtils/testDefaults.ts @@ -5,7 +5,7 @@ import { Section, Task, User, - Comment, + RawComment, Attachment, Duration, Deadline, @@ -34,6 +34,7 @@ const DEFAULT_USER_NAME = 'A User' const DEFAULT_USER_EMAIL = 'atestuser@doist.com' const DEFAULT_COMMENT_ID = '4' const DEFAULT_COMMENT_CONTENT = 'A comment' +const DEFAULT_COMMENT_REACTIONS = { '👍': ['1234', '5678'] } export const DEFAULT_AUTH_TOKEN = 'AToken' export const DEFAULT_REQUEST_ID = 'ARequestID' @@ -146,9 +147,16 @@ export const PROJECT_WITH_OPTIONALS_AS_NULL: Project = { export const DEFAULT_SECTION: Section = { id: DEFAULT_SECTION_ID, - name: DEFAULT_SECTION_NAME, - order: DEFAULT_ORDER, + userId: DEFAULT_USER_ID, projectId: DEFAULT_PROJECT_ID, + addedAt: '2025-03-28T14:01:23.334881Z', + updatedAt: '2025-03-28T14:01:23.334885Z', + archivedAt: null, + name: DEFAULT_SECTION_NAME, + sectionOrder: DEFAULT_ORDER, + isArchived: false, + isDeleted: false, + isCollapsed: false, } export const INVALID_SECTION = { @@ -192,30 +200,46 @@ export const INVALID_ATTACHMENT = { uploadState: 'something random', } -export const DEFAULT_COMMENT: Comment = { +export const DEFAULT_RAW_COMMENT: RawComment = { id: DEFAULT_COMMENT_ID, + postedUid: DEFAULT_USER_ID, content: DEFAULT_COMMENT_CONTENT, - taskId: null, - projectId: DEFAULT_PROJECT_ID, - attachment: DEFAULT_ATTACHMENT, + fileAttachment: DEFAULT_ATTACHMENT, + uidsToNotify: null, + isDeleted: false, postedAt: DEFAULT_DATE, + reactions: DEFAULT_COMMENT_REACTIONS, + itemId: DEFAULT_TASK_ID, +} + +export const DEFAULT_COMMENT = { + ...DEFAULT_RAW_COMMENT, + taskId: DEFAULT_RAW_COMMENT.itemId, + itemId: undefined, } export const INVALID_COMMENT = { - ...DEFAULT_COMMENT, - attachment: INVALID_ATTACHMENT, + ...DEFAULT_RAW_COMMENT, + isDeleted: 'true', } -export const COMMENT_WITH_OPTIONALS_AS_NULL_TASK: Comment = { - ...DEFAULT_COMMENT, - projectId: null, - attachment: null, +export const RAW_COMMENT_WITH_OPTIONALS_AS_NULL_TASK: RawComment = { + ...DEFAULT_RAW_COMMENT, + fileAttachment: null, + uidsToNotify: null, + reactions: null, } -export const COMMENT_WITH_ATTACHMENT_WITH_OPTIONALS_AS_NULL: Comment = { - ...DEFAULT_COMMENT, - attachment: { - ...DEFAULT_ATTACHMENT, +export const COMMENT_WITH_OPTIONALS_AS_NULL_TASK = { + ...RAW_COMMENT_WITH_OPTIONALS_AS_NULL_TASK, + taskId: RAW_COMMENT_WITH_OPTIONALS_AS_NULL_TASK.itemId, + itemId: undefined, +} + +export const RAW_COMMENT_WITH_ATTACHMENT_WITH_OPTIONALS_AS_NULL: RawComment = { + ...DEFAULT_RAW_COMMENT, + fileAttachment: { + resourceType: 'file', fileName: null, fileSize: null, fileType: null, @@ -229,8 +253,19 @@ export const COMMENT_WITH_ATTACHMENT_WITH_OPTIONALS_AS_NULL: Comment = { }, } -export const COMMENT_WITH_OPTIONALS_AS_NULL_PROJECT: Comment = { - ...DEFAULT_COMMENT, - taskId: null, - attachment: null, +export const COMMENT_WITH_ATTACHMENT_WITH_OPTIONALS_AS_NULL = { + ...RAW_COMMENT_WITH_ATTACHMENT_WITH_OPTIONALS_AS_NULL, + taskId: RAW_COMMENT_WITH_ATTACHMENT_WITH_OPTIONALS_AS_NULL.itemId, + itemId: undefined, +} + +export const RAW_COMMENT_WITH_OPTIONALS_AS_NULL_PROJECT: RawComment = { + ...DEFAULT_RAW_COMMENT, + itemId: undefined, + projectId: DEFAULT_PROJECT_ID, +} + +export const COMMENT_WITH_OPTIONALS_AS_NULL_PROJECT = { + ...RAW_COMMENT_WITH_OPTIONALS_AS_NULL_PROJECT, + taskId: undefined, } diff --git a/src/types/entities.ts b/src/types/entities.ts index 1c79b9c..84afca1 100644 --- a/src/types/entities.ts +++ b/src/types/entities.ts @@ -89,9 +89,16 @@ export type ProjectViewStyle = 'list' | 'board' | 'calendar' export const SectionSchema = z.object({ id: z.string(), - order: z.number().int(), - name: z.string(), + userId: z.string(), projectId: z.string(), + addedAt: z.string(), + updatedAt: z.string(), + archivedAt: z.string().nullable(), + name: z.string(), + sectionOrder: z.number().int(), + isArchived: z.boolean(), + isDeleted: z.boolean(), + isCollapsed: z.boolean(), }) /** * Represents a section within a project, used to group tasks. @@ -135,14 +142,47 @@ export const AttachmentSchema = z */ export interface Attachment extends z.infer {} -export const CommentSchema = z.object({ - id: z.string(), - taskId: z.string().nullable(), - projectId: z.string().nullable(), - content: z.string(), - postedAt: z.string(), - attachment: AttachmentSchema.nullable(), +export const RawCommentSchema = z + .object({ + id: z.string(), + itemId: z.string().optional(), + projectId: z.string().optional(), + content: z.string(), + postedAt: z.string(), + fileAttachment: AttachmentSchema.nullable(), + postedUid: z.string(), + uidsToNotify: z.array(z.string()).nullable(), + reactions: z.record(z.string(), z.array(z.string())).nullable(), + isDeleted: z.boolean(), + }) + .refine( + (data) => { + // At least one of itemId or projectId must be present + return ( + (data.itemId !== undefined && data.projectId === undefined) || + (data.itemId === undefined && data.projectId !== undefined) + ) + }, + { + message: 'Exactly one of itemId or projectId must be provided', + }, + ) + +/** + * Represents the raw comment data as received from the API before transformation. + * @see https://developer.todoist.com/sync/v9/#notes + */ +export interface RawComment extends z.infer {} + +export const CommentSchema = RawCommentSchema.transform((data) => { + const { itemId, ...rest } = data + return { + ...rest, + // Map itemId to taskId for backwards compatibility + taskId: itemId, + } }) + /** * Represents a comment on a task or project in Todoist. * @see https://developer.todoist.com/sync/v9/#notes diff --git a/src/utils/validators.test.ts b/src/utils/validators.test.ts index 9517121..6f9c96b 100644 --- a/src/utils/validators.test.ts +++ b/src/utils/validators.test.ts @@ -12,6 +12,7 @@ import { INVALID_COMMENT, DEFAULT_USER, INVALID_USER, + DEFAULT_RAW_COMMENT, } from '../testUtils/testDefaults' import { validateTask, @@ -155,7 +156,7 @@ describe('validators', () => { describe('validateComment', () => { test('validation passes for a valid comment', () => { - const result = validateComment(DEFAULT_COMMENT) + const result = validateComment(DEFAULT_RAW_COMMENT) expect(result).toEqual(DEFAULT_COMMENT) }) @@ -173,7 +174,7 @@ describe('validators', () => { }) test('validation passes for valid comment array', () => { - const result = validateCommentArray([DEFAULT_COMMENT]) + const result = validateCommentArray([DEFAULT_RAW_COMMENT]) expect(result).toEqual([DEFAULT_COMMENT]) })