Skip to content

Commit ee23ba3

Browse files
chore: Switch to dates and modern naming conventions (#353)
1 parent 0466d4a commit ee23ba3

File tree

9 files changed

+527
-10
lines changed

9 files changed

+527
-10
lines changed

src/TodoistApi.activities.test.ts

Lines changed: 261 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ describe('TodoistApi activity endpoints', () => {
8888
const result = await api.getActivityLogs()
8989

9090
expect(result.results).toHaveLength(2)
91-
expect(result.results[0].objectType).toBe('item')
91+
expect(result.results[0].objectType).toBe('task') // Converted from 'item' to 'task'
9292
expect(result.results[0].eventType).toBe('added')
9393
expect(result.nextCursor).toBeNull()
9494
})
@@ -161,5 +161,265 @@ describe('TodoistApi activity endpoints', () => {
161161
another_unknown: 123,
162162
})
163163
})
164+
165+
test('converts Date objects to YYYY-MM-DD strings', async () => {
166+
const requestMock = setupRestClientMock({
167+
results: DEFAULT_ACTIVITY_RESPONSE,
168+
nextCursor: null,
169+
})
170+
const api = getTarget()
171+
172+
const sinceDate = new Date('2025-01-15T10:30:00Z')
173+
const untilDate = new Date('2025-01-20T15:45:00Z')
174+
175+
await api.getActivityLogs({
176+
since: sinceDate,
177+
until: untilDate,
178+
})
179+
180+
expect(requestMock).toHaveBeenCalledWith(
181+
'GET',
182+
getSyncBaseUri(),
183+
ENDPOINT_REST_ACTIVITIES,
184+
DEFAULT_AUTH_TOKEN,
185+
{
186+
since: '2025-01-15',
187+
until: '2025-01-20',
188+
},
189+
)
190+
})
191+
192+
test('leaves string dates as-is for backward compatibility', async () => {
193+
const requestMock = setupRestClientMock({
194+
results: DEFAULT_ACTIVITY_RESPONSE,
195+
nextCursor: null,
196+
})
197+
const api = getTarget()
198+
199+
await api.getActivityLogs({
200+
since: '2025-01-15',
201+
until: '2025-01-20',
202+
})
203+
204+
expect(requestMock).toHaveBeenCalledWith(
205+
'GET',
206+
getSyncBaseUri(),
207+
ENDPOINT_REST_ACTIVITIES,
208+
DEFAULT_AUTH_TOKEN,
209+
{
210+
since: '2025-01-15',
211+
until: '2025-01-20',
212+
},
213+
)
214+
})
215+
216+
test('converts Date objects with correct timezone handling', async () => {
217+
const requestMock = setupRestClientMock({
218+
results: DEFAULT_ACTIVITY_RESPONSE,
219+
nextCursor: null,
220+
})
221+
const api = getTarget()
222+
223+
// Test with a date that has time components
224+
const sinceDate = new Date(2025, 0, 15, 23, 59, 59) // January 15, 2025, 23:59:59 local time
225+
226+
await api.getActivityLogs({
227+
since: sinceDate,
228+
})
229+
230+
const expectedSince = `${sinceDate.getFullYear()}-01-15`
231+
232+
expect(requestMock).toHaveBeenCalledWith(
233+
'GET',
234+
getSyncBaseUri(),
235+
ENDPOINT_REST_ACTIVITIES,
236+
DEFAULT_AUTH_TOKEN,
237+
{
238+
since: expectedSince,
239+
},
240+
)
241+
})
242+
243+
test('converts modern objectType "task" to legacy "item" in API request', async () => {
244+
const requestMock = setupRestClientMock({
245+
results: DEFAULT_ACTIVITY_RESPONSE,
246+
nextCursor: null,
247+
})
248+
const api = getTarget()
249+
250+
await api.getActivityLogs({
251+
objectType: 'task',
252+
})
253+
254+
expect(requestMock).toHaveBeenCalledWith(
255+
'GET',
256+
getSyncBaseUri(),
257+
ENDPOINT_REST_ACTIVITIES,
258+
DEFAULT_AUTH_TOKEN,
259+
{
260+
objectType: 'item',
261+
},
262+
)
263+
})
264+
265+
test('converts modern objectType "comment" to legacy "note" in API request', async () => {
266+
const requestMock = setupRestClientMock({
267+
results: DEFAULT_ACTIVITY_RESPONSE,
268+
nextCursor: null,
269+
})
270+
const api = getTarget()
271+
272+
await api.getActivityLogs({
273+
objectType: 'comment',
274+
})
275+
276+
expect(requestMock).toHaveBeenCalledWith(
277+
'GET',
278+
getSyncBaseUri(),
279+
ENDPOINT_REST_ACTIVITIES,
280+
DEFAULT_AUTH_TOKEN,
281+
{
282+
objectType: 'note',
283+
},
284+
)
285+
})
286+
287+
test('leaves project objectType unchanged', async () => {
288+
const requestMock = setupRestClientMock({
289+
results: DEFAULT_ACTIVITY_RESPONSE,
290+
nextCursor: null,
291+
})
292+
const api = getTarget()
293+
294+
await api.getActivityLogs({
295+
objectType: 'project',
296+
})
297+
298+
expect(requestMock).toHaveBeenCalledWith(
299+
'GET',
300+
getSyncBaseUri(),
301+
ENDPOINT_REST_ACTIVITIES,
302+
DEFAULT_AUTH_TOKEN,
303+
{
304+
objectType: 'project',
305+
},
306+
)
307+
})
308+
309+
test('converts legacy "item" to modern "task" in response', async () => {
310+
setupRestClientMock({
311+
results: [
312+
{
313+
id: '1',
314+
objectType: 'item',
315+
objectId: '123',
316+
eventType: 'added',
317+
eventDate: '2025-01-10T10:00:00Z',
318+
parentProjectId: null,
319+
parentItemId: null,
320+
initiatorId: 'user123',
321+
extraData: {},
322+
},
323+
],
324+
nextCursor: null,
325+
})
326+
const api = getTarget()
327+
328+
const result = await api.getActivityLogs()
329+
330+
expect(result.results[0].objectType).toBe('task')
331+
})
332+
333+
test('converts legacy "note" to modern "comment" in response', async () => {
334+
setupRestClientMock({
335+
results: [
336+
{
337+
id: '1',
338+
objectType: 'note',
339+
objectId: '456',
340+
eventType: 'added',
341+
eventDate: '2025-01-10T10:00:00Z',
342+
parentProjectId: null,
343+
parentItemId: null,
344+
initiatorId: 'user123',
345+
extraData: {},
346+
},
347+
],
348+
nextCursor: null,
349+
})
350+
const api = getTarget()
351+
352+
const result = await api.getActivityLogs()
353+
354+
expect(result.results[0].objectType).toBe('comment')
355+
})
356+
357+
test('leaves project objectType unchanged in response', async () => {
358+
setupRestClientMock({
359+
results: [
360+
{
361+
id: '1',
362+
objectType: 'project',
363+
objectId: '789',
364+
eventType: 'updated',
365+
eventDate: '2025-01-10T10:00:00Z',
366+
parentProjectId: null,
367+
parentItemId: null,
368+
initiatorId: 'user123',
369+
extraData: {},
370+
},
371+
],
372+
nextCursor: null,
373+
})
374+
const api = getTarget()
375+
376+
const result = await api.getActivityLogs()
377+
378+
expect(result.results[0].objectType).toBe('project')
379+
})
380+
381+
test('supports backward compatibility with legacy "item" in request', async () => {
382+
const requestMock = setupRestClientMock({
383+
results: DEFAULT_ACTIVITY_RESPONSE,
384+
nextCursor: null,
385+
})
386+
const api = getTarget()
387+
388+
await api.getActivityLogs({
389+
objectType: 'item',
390+
})
391+
392+
expect(requestMock).toHaveBeenCalledWith(
393+
'GET',
394+
getSyncBaseUri(),
395+
ENDPOINT_REST_ACTIVITIES,
396+
DEFAULT_AUTH_TOKEN,
397+
{
398+
objectType: 'item',
399+
},
400+
)
401+
})
402+
403+
test('supports backward compatibility with legacy "note" in request', async () => {
404+
const requestMock = setupRestClientMock({
405+
results: DEFAULT_ACTIVITY_RESPONSE,
406+
nextCursor: null,
407+
})
408+
const api = getTarget()
409+
410+
await api.getActivityLogs({
411+
objectType: 'note',
412+
})
413+
414+
expect(requestMock).toHaveBeenCalledWith(
415+
'GET',
416+
getSyncBaseUri(),
417+
ENDPOINT_REST_ACTIVITIES,
418+
DEFAULT_AUTH_TOKEN,
419+
{
420+
objectType: 'note',
421+
},
422+
)
423+
})
164424
})
165425
})

src/TodoistApi.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ import {
9191
validateProductivityStats,
9292
validateActivityEventArray,
9393
} from './utils/validators'
94+
import { formatDateToYYYYMMDD } from './utils/urlHelpers'
95+
import { normalizeObjectTypeForApi, denormalizeObjectTypeFromApi } from './utils/activity-helpers'
9496
import { z } from 'zod'
9597

9698
import { v4 as uuidv4 } from 'uuid'
@@ -1072,18 +1074,35 @@ export class TodoistApi {
10721074
* @returns A promise that resolves to a paginated response of activity events.
10731075
*/
10741076
async getActivityLogs(args: GetActivityLogsArgs = {}): Promise<GetActivityLogsResponse> {
1077+
// Convert Date objects to YYYY-MM-DD strings and modern object types to legacy API types
1078+
const processedArgs = {
1079+
...args,
1080+
...(args.since instanceof Date && { since: formatDateToYYYYMMDD(args.since) }),
1081+
...(args.until instanceof Date && { until: formatDateToYYYYMMDD(args.until) }),
1082+
...(args.objectType && { objectType: normalizeObjectTypeForApi(args.objectType) }),
1083+
}
1084+
10751085
const {
10761086
data: { results, nextCursor },
10771087
} = await request<GetActivityLogsResponse>(
10781088
'GET',
10791089
this.syncApiBase,
10801090
ENDPOINT_REST_ACTIVITIES,
10811091
this.authToken,
1082-
args,
1092+
processedArgs as Record<string, unknown>,
10831093
)
10841094

1095+
// Convert legacy API object types back to modern SDK types
1096+
const normalizedResults = results.map((event) => {
1097+
const normalizedType = denormalizeObjectTypeFromApi(event.objectType)
1098+
return {
1099+
...event,
1100+
objectType: normalizedType || event.objectType,
1101+
}
1102+
}) as unknown[]
1103+
10851104
return {
1086-
results: validateActivityEventArray(results),
1105+
results: validateActivityEventArray(normalizedResults),
10871106
nextCursor,
10881107
}
10891108
}

src/types/entities.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -402,10 +402,29 @@ export const ColorSchema = z.object({
402402
*/
403403
export type Color = z.infer<typeof ColorSchema>
404404

405+
/**
406+
* @deprecated Use 'task' instead. This will be removed in the next major version.
407+
*/
408+
type DeprecatedItem = 'item'
409+
410+
/**
411+
* @deprecated Use 'comment' instead. This will be removed in the next major version.
412+
*/
413+
type DeprecatedNote = 'note'
414+
405415
/**
406416
* Type hints for known object types. Accepts any string for forward compatibility.
417+
* Supports both modern naming ('task', 'comment') and legacy naming ('item', 'note').
418+
*
419+
* **Note**: The legacy values 'item' and 'note' are deprecated. Use 'task' and 'comment' instead.
407420
*/
408-
export type ActivityObjectType = 'item' | 'note' | 'project' | (string & Record<string, never>)
421+
export type ActivityObjectType =
422+
| 'task'
423+
| 'comment'
424+
| 'project'
425+
| DeprecatedItem
426+
| DeprecatedNote
427+
| (string & Record<string, never>)
409428

410429
/**
411430
* Type hints for known event types. Accepts any string for forward compatibility.

0 commit comments

Comments
 (0)