@doist/todoist-ai 4.15.1 → 4.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/filter-helpers.d.ts +1 -1
- package/dist/index.d.ts +175 -175
- package/dist/index.js +61 -81
- package/dist/main.js +15 -23
- package/dist/mcp-helpers.d.ts +4 -4
- package/dist/mcp-server-6tm7Rhyz.js +2840 -0
- package/dist/todoist-tool.d.ts +2 -2
- package/dist/tool-helpers.d.ts +1 -1
- package/dist/tools/add-comments.d.ts +1 -1
- package/dist/tools/add-comments.d.ts.map +1 -1
- package/dist/tools/add-projects.d.ts +4 -4
- package/dist/tools/add-projects.d.ts.map +1 -1
- package/dist/tools/add-sections.d.ts +1 -1
- package/dist/tools/add-sections.d.ts.map +1 -1
- package/dist/tools/add-tasks.d.ts +4 -4
- package/dist/tools/add-tasks.d.ts.map +1 -1
- package/dist/tools/complete-tasks.d.ts +1 -1
- package/dist/tools/complete-tasks.d.ts.map +1 -1
- package/dist/tools/delete-object.d.ts +3 -3
- package/dist/tools/delete-object.d.ts.map +1 -1
- package/dist/tools/fetch.d.ts +1 -1
- package/dist/tools/find-activity.d.ts +5 -5
- package/dist/tools/find-activity.d.ts.map +1 -1
- package/dist/tools/find-comments.d.ts +2 -2
- package/dist/tools/find-comments.d.ts.map +1 -1
- package/dist/tools/find-completed-tasks.d.ts +3 -3
- package/dist/tools/find-completed-tasks.d.ts.map +1 -1
- package/dist/tools/find-project-collaborators.d.ts +2 -2
- package/dist/tools/find-projects.d.ts +1 -1
- package/dist/tools/find-projects.d.ts.map +1 -1
- package/dist/tools/find-sections.d.ts +1 -1
- package/dist/tools/find-sections.d.ts.map +1 -1
- package/dist/tools/find-tasks-by-date.d.ts +1 -1
- package/dist/tools/find-tasks-by-date.d.ts.map +1 -1
- package/dist/tools/find-tasks.d.ts +3 -3
- package/dist/tools/find-tasks.d.ts.map +1 -1
- package/dist/tools/get-overview.d.ts +1 -1
- package/dist/tools/manage-assignments.d.ts +1 -1
- package/dist/tools/search.d.ts +1 -1
- package/dist/tools/update-comments.d.ts +4 -4
- package/dist/tools/update-comments.d.ts.map +1 -1
- package/dist/tools/update-projects.d.ts +1 -1
- package/dist/tools/update-projects.d.ts.map +1 -1
- package/dist/tools/update-sections.d.ts +4 -4
- package/dist/tools/update-sections.d.ts.map +1 -1
- package/dist/tools/update-tasks.d.ts +7 -7
- package/dist/tools/update-tasks.d.ts.map +1 -1
- package/dist/tools/user-info.d.ts +1 -1
- package/dist/utils/assignment-validator.d.ts +2 -2
- package/dist/utils/response-builders.d.ts +1 -3
- package/dist/utils/response-builders.d.ts.map +1 -1
- package/dist/utils/test-helpers.d.ts +1 -1
- package/dist/utils/user-resolver.d.ts +1 -1
- package/package.json +11 -9
- package/dist/filter-helpers.js +0 -79
- package/dist/mcp-helpers.js +0 -71
- package/dist/mcp-server.js +0 -142
- package/dist/todoist-tool.js +0 -1
- package/dist/tool-helpers.js +0 -125
- package/dist/tool-helpers.test.d.ts +0 -2
- package/dist/tool-helpers.test.d.ts.map +0 -1
- package/dist/tool-helpers.test.js +0 -223
- package/dist/tools/__tests__/add-comments.test.d.ts +0 -2
- package/dist/tools/__tests__/add-comments.test.d.ts.map +0 -1
- package/dist/tools/__tests__/add-comments.test.js +0 -241
- package/dist/tools/__tests__/add-projects.test.d.ts +0 -2
- package/dist/tools/__tests__/add-projects.test.d.ts.map +0 -1
- package/dist/tools/__tests__/add-projects.test.js +0 -174
- package/dist/tools/__tests__/add-sections.test.d.ts +0 -2
- package/dist/tools/__tests__/add-sections.test.d.ts.map +0 -1
- package/dist/tools/__tests__/add-sections.test.js +0 -185
- package/dist/tools/__tests__/add-tasks.test.d.ts +0 -2
- package/dist/tools/__tests__/add-tasks.test.d.ts.map +0 -1
- package/dist/tools/__tests__/add-tasks.test.js +0 -533
- package/dist/tools/__tests__/assignment-integration.test.d.ts +0 -2
- package/dist/tools/__tests__/assignment-integration.test.d.ts.map +0 -1
- package/dist/tools/__tests__/assignment-integration.test.js +0 -428
- package/dist/tools/__tests__/complete-tasks.test.d.ts +0 -2
- package/dist/tools/__tests__/complete-tasks.test.d.ts.map +0 -1
- package/dist/tools/__tests__/complete-tasks.test.js +0 -206
- package/dist/tools/__tests__/delete-object.test.d.ts +0 -2
- package/dist/tools/__tests__/delete-object.test.d.ts.map +0 -1
- package/dist/tools/__tests__/delete-object.test.js +0 -110
- package/dist/tools/__tests__/fetch.test.d.ts +0 -2
- package/dist/tools/__tests__/fetch.test.d.ts.map +0 -1
- package/dist/tools/__tests__/fetch.test.js +0 -279
- package/dist/tools/__tests__/find-activity.test.d.ts +0 -2
- package/dist/tools/__tests__/find-activity.test.d.ts.map +0 -1
- package/dist/tools/__tests__/find-activity.test.js +0 -229
- package/dist/tools/__tests__/find-comments.test.d.ts +0 -2
- package/dist/tools/__tests__/find-comments.test.d.ts.map +0 -1
- package/dist/tools/__tests__/find-comments.test.js +0 -236
- package/dist/tools/__tests__/find-completed-tasks.test.d.ts +0 -2
- package/dist/tools/__tests__/find-completed-tasks.test.d.ts.map +0 -1
- package/dist/tools/__tests__/find-completed-tasks.test.js +0 -324
- package/dist/tools/__tests__/find-projects.test.d.ts +0 -2
- package/dist/tools/__tests__/find-projects.test.d.ts.map +0 -1
- package/dist/tools/__tests__/find-projects.test.js +0 -154
- package/dist/tools/__tests__/find-sections.test.d.ts +0 -2
- package/dist/tools/__tests__/find-sections.test.d.ts.map +0 -1
- package/dist/tools/__tests__/find-sections.test.js +0 -245
- package/dist/tools/__tests__/find-tasks-by-date.test.d.ts +0 -2
- package/dist/tools/__tests__/find-tasks-by-date.test.d.ts.map +0 -1
- package/dist/tools/__tests__/find-tasks-by-date.test.js +0 -528
- package/dist/tools/__tests__/find-tasks.test.d.ts +0 -2
- package/dist/tools/__tests__/find-tasks.test.d.ts.map +0 -1
- package/dist/tools/__tests__/find-tasks.test.js +0 -771
- package/dist/tools/__tests__/get-overview.test.d.ts +0 -2
- package/dist/tools/__tests__/get-overview.test.d.ts.map +0 -1
- package/dist/tools/__tests__/get-overview.test.js +0 -225
- package/dist/tools/__tests__/search.test.d.ts +0 -2
- package/dist/tools/__tests__/search.test.d.ts.map +0 -1
- package/dist/tools/__tests__/search.test.js +0 -206
- package/dist/tools/__tests__/update-comments.test.d.ts +0 -2
- package/dist/tools/__tests__/update-comments.test.d.ts.map +0 -1
- package/dist/tools/__tests__/update-comments.test.js +0 -294
- package/dist/tools/__tests__/update-projects.test.d.ts +0 -2
- package/dist/tools/__tests__/update-projects.test.d.ts.map +0 -1
- package/dist/tools/__tests__/update-projects.test.js +0 -217
- package/dist/tools/__tests__/update-sections.test.d.ts +0 -2
- package/dist/tools/__tests__/update-sections.test.d.ts.map +0 -1
- package/dist/tools/__tests__/update-sections.test.js +0 -169
- package/dist/tools/__tests__/update-tasks.test.d.ts +0 -2
- package/dist/tools/__tests__/update-tasks.test.d.ts.map +0 -1
- package/dist/tools/__tests__/update-tasks.test.js +0 -788
- package/dist/tools/__tests__/user-info.test.d.ts +0 -2
- package/dist/tools/__tests__/user-info.test.d.ts.map +0 -1
- package/dist/tools/__tests__/user-info.test.js +0 -139
- package/dist/tools/add-comments.js +0 -79
- package/dist/tools/add-projects.js +0 -63
- package/dist/tools/add-sections.js +0 -61
- package/dist/tools/add-tasks.js +0 -160
- package/dist/tools/complete-tasks.js +0 -68
- package/dist/tools/delete-object.js +0 -79
- package/dist/tools/fetch.js +0 -102
- package/dist/tools/find-activity.js +0 -221
- package/dist/tools/find-comments.js +0 -143
- package/dist/tools/find-completed-tasks.js +0 -161
- package/dist/tools/find-project-collaborators.js +0 -151
- package/dist/tools/find-projects.js +0 -101
- package/dist/tools/find-sections.js +0 -96
- package/dist/tools/find-tasks-by-date.js +0 -198
- package/dist/tools/find-tasks.js +0 -329
- package/dist/tools/get-overview.js +0 -249
- package/dist/tools/manage-assignments.js +0 -337
- package/dist/tools/search.js +0 -65
- package/dist/tools/update-comments.js +0 -82
- package/dist/tools/update-projects.js +0 -84
- package/dist/tools/update-sections.js +0 -70
- package/dist/tools/update-tasks.js +0 -170
- package/dist/tools/user-info.js +0 -142
- package/dist/utils/assignment-validator.js +0 -253
- package/dist/utils/constants.js +0 -45
- package/dist/utils/duration-parser.js +0 -96
- package/dist/utils/duration-parser.test.d.ts +0 -2
- package/dist/utils/duration-parser.test.d.ts.map +0 -1
- package/dist/utils/duration-parser.test.js +0 -147
- package/dist/utils/labels.js +0 -18
- package/dist/utils/priorities.js +0 -20
- package/dist/utils/response-builders.js +0 -210
- package/dist/utils/sanitize-data.js +0 -37
- package/dist/utils/sanitize-data.test.d.ts +0 -2
- package/dist/utils/sanitize-data.test.d.ts.map +0 -1
- package/dist/utils/sanitize-data.test.js +0 -93
- package/dist/utils/test-helpers.js +0 -237
- package/dist/utils/tool-names.js +0 -40
- package/dist/utils/user-resolver.js +0 -179
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Duration parser utility for converting human-readable duration strings
|
|
3
|
-
* to minutes using a restricted, language-neutral syntax.
|
|
4
|
-
*
|
|
5
|
-
* Supported formats:
|
|
6
|
-
* - "2h" (hours only)
|
|
7
|
-
* - "90m" (minutes only)
|
|
8
|
-
* - "2h30m" (hours + minutes)
|
|
9
|
-
* - "1.5h" (decimal hours)
|
|
10
|
-
* - Supports optional spaces: "2h 30m"
|
|
11
|
-
*/
|
|
12
|
-
export class DurationParseError extends Error {
|
|
13
|
-
constructor(input, reason) {
|
|
14
|
-
super(`Invalid duration format "${input}": ${reason}`);
|
|
15
|
-
this.name = 'DurationParseError';
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* Parses duration string in restricted syntax to minutes.
|
|
20
|
-
* Max duration: 1440 minutes (24 hours)
|
|
21
|
-
*
|
|
22
|
-
* @param durationStr - Duration string like "2h30m", "45m", "1.5h"
|
|
23
|
-
* @returns Parsed duration in minutes
|
|
24
|
-
* @throws DurationParseError for invalid formats
|
|
25
|
-
*/
|
|
26
|
-
export function parseDuration(durationStr) {
|
|
27
|
-
if (!durationStr || typeof durationStr !== 'string') {
|
|
28
|
-
throw new DurationParseError(durationStr, 'Duration must be a non-empty string');
|
|
29
|
-
}
|
|
30
|
-
// Remove all spaces and convert to lowercase
|
|
31
|
-
const normalized = durationStr.trim().toLowerCase().replace(/\s+/g, '');
|
|
32
|
-
// Check for empty string after trimming
|
|
33
|
-
if (!normalized) {
|
|
34
|
-
throw new DurationParseError(durationStr, 'Duration must be a non-empty string');
|
|
35
|
-
}
|
|
36
|
-
// Validate format with strict ordering: hours must come before minutes
|
|
37
|
-
// This regex ensures: optional hours followed by optional minutes, no duplicates
|
|
38
|
-
const match = normalized.match(/^(?:(\d+(?:\.\d+)?)h)?(?:(\d+(?:\.\d+)?)m)?$/);
|
|
39
|
-
if (!match || (!match[1] && !match[2])) {
|
|
40
|
-
throw new DurationParseError(durationStr, 'Use format like "2h", "30m", "2h30m", or "1.5h"');
|
|
41
|
-
}
|
|
42
|
-
let totalMinutes = 0;
|
|
43
|
-
const [, hoursStr, minutesStr] = match;
|
|
44
|
-
// Parse hours if present
|
|
45
|
-
if (hoursStr) {
|
|
46
|
-
const hours = Number.parseFloat(hoursStr);
|
|
47
|
-
if (Number.isNaN(hours) || hours < 0) {
|
|
48
|
-
throw new DurationParseError(durationStr, 'Hours must be a positive number');
|
|
49
|
-
}
|
|
50
|
-
totalMinutes += hours * 60;
|
|
51
|
-
}
|
|
52
|
-
// Parse minutes if present
|
|
53
|
-
if (minutesStr) {
|
|
54
|
-
const minutes = Number.parseFloat(minutesStr);
|
|
55
|
-
if (Number.isNaN(minutes) || minutes < 0) {
|
|
56
|
-
throw new DurationParseError(durationStr, 'Minutes must be a positive number');
|
|
57
|
-
}
|
|
58
|
-
// Don't allow decimal minutes
|
|
59
|
-
if (minutes % 1 !== 0) {
|
|
60
|
-
throw new DurationParseError(durationStr, 'Minutes must be a whole number (use decimal hours instead)');
|
|
61
|
-
}
|
|
62
|
-
totalMinutes += minutes;
|
|
63
|
-
}
|
|
64
|
-
// The regex already ensures at least one unit is present
|
|
65
|
-
// Round to nearest minute (handles decimal hours)
|
|
66
|
-
totalMinutes = Math.round(totalMinutes);
|
|
67
|
-
// Validate minimum duration
|
|
68
|
-
if (totalMinutes === 0) {
|
|
69
|
-
throw new DurationParseError(durationStr, 'Duration must be greater than 0 minutes');
|
|
70
|
-
}
|
|
71
|
-
// Validate maximum duration (24 hours = 1440 minutes)
|
|
72
|
-
if (totalMinutes > 1440) {
|
|
73
|
-
throw new DurationParseError(durationStr, 'Duration cannot exceed 24 hours (1440 minutes)');
|
|
74
|
-
}
|
|
75
|
-
return { minutes: totalMinutes };
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* Formats minutes back to a human-readable duration string.
|
|
79
|
-
* Used when returning task data to LLMs.
|
|
80
|
-
*
|
|
81
|
-
* @param minutes - Duration in minutes
|
|
82
|
-
* @returns Formatted duration string like "2h30m" or "45m"
|
|
83
|
-
*/
|
|
84
|
-
export function formatDuration(minutes) {
|
|
85
|
-
if (minutes <= 0)
|
|
86
|
-
return '0m';
|
|
87
|
-
const hours = Math.floor(minutes / 60);
|
|
88
|
-
const remainingMinutes = minutes % 60;
|
|
89
|
-
if (hours === 0) {
|
|
90
|
-
return `${remainingMinutes}m`;
|
|
91
|
-
}
|
|
92
|
-
if (remainingMinutes === 0) {
|
|
93
|
-
return `${hours}h`;
|
|
94
|
-
}
|
|
95
|
-
return `${hours}h${remainingMinutes}m`;
|
|
96
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"duration-parser.test.d.ts","sourceRoot":"","sources":["../../src/utils/duration-parser.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
import { DurationParseError, formatDuration, parseDuration } from './duration-parser.js';
|
|
2
|
-
describe('parseDuration', () => {
|
|
3
|
-
describe('valid formats', () => {
|
|
4
|
-
it('should parse hours only', () => {
|
|
5
|
-
expect(parseDuration('2h')).toEqual({ minutes: 120 });
|
|
6
|
-
expect(parseDuration('1h')).toEqual({ minutes: 60 });
|
|
7
|
-
expect(parseDuration('24h')).toEqual({ minutes: 1440 });
|
|
8
|
-
});
|
|
9
|
-
it('should parse minutes only', () => {
|
|
10
|
-
expect(parseDuration('90m')).toEqual({ minutes: 90 });
|
|
11
|
-
expect(parseDuration('45m')).toEqual({ minutes: 45 });
|
|
12
|
-
expect(parseDuration('1m')).toEqual({ minutes: 1 });
|
|
13
|
-
expect(parseDuration('1440m')).toEqual({ minutes: 1440 });
|
|
14
|
-
});
|
|
15
|
-
it('should parse hours and minutes combined', () => {
|
|
16
|
-
expect(parseDuration('2h30m')).toEqual({ minutes: 150 });
|
|
17
|
-
expect(parseDuration('1h45m')).toEqual({ minutes: 105 });
|
|
18
|
-
expect(parseDuration('0h30m')).toEqual({ minutes: 30 });
|
|
19
|
-
expect(parseDuration('23h59m')).toEqual({ minutes: 1439 });
|
|
20
|
-
});
|
|
21
|
-
it('should parse decimal hours', () => {
|
|
22
|
-
expect(parseDuration('1.5h')).toEqual({ minutes: 90 });
|
|
23
|
-
expect(parseDuration('2.25h')).toEqual({ minutes: 135 });
|
|
24
|
-
expect(parseDuration('0.5h')).toEqual({ minutes: 30 });
|
|
25
|
-
expect(parseDuration('0.75h')).toEqual({ minutes: 45 });
|
|
26
|
-
});
|
|
27
|
-
it('should handle spaces in input', () => {
|
|
28
|
-
expect(parseDuration('2h 30m')).toEqual({ minutes: 150 });
|
|
29
|
-
expect(parseDuration(' 1h45m ')).toEqual({ minutes: 105 });
|
|
30
|
-
expect(parseDuration(' 2h ')).toEqual({ minutes: 120 });
|
|
31
|
-
expect(parseDuration(' 90m ')).toEqual({ minutes: 90 });
|
|
32
|
-
});
|
|
33
|
-
it('should handle case insensitive input', () => {
|
|
34
|
-
expect(parseDuration('2H')).toEqual({ minutes: 120 });
|
|
35
|
-
expect(parseDuration('90M')).toEqual({ minutes: 90 });
|
|
36
|
-
expect(parseDuration('2H30M')).toEqual({ minutes: 150 });
|
|
37
|
-
expect(parseDuration('1.5H')).toEqual({ minutes: 90 });
|
|
38
|
-
});
|
|
39
|
-
it('should round decimal minutes from decimal hours', () => {
|
|
40
|
-
expect(parseDuration('1.33h')).toEqual({ minutes: 80 }); // 1.33 * 60 = 79.8 -> 80
|
|
41
|
-
expect(parseDuration('1.67h')).toEqual({ minutes: 100 }); // 1.67 * 60 = 100.2 -> 100
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
describe('invalid formats', () => {
|
|
45
|
-
it('should throw error for empty or null input', () => {
|
|
46
|
-
expect(() => parseDuration('')).toThrow(DurationParseError);
|
|
47
|
-
expect(() => parseDuration(' ')).toThrow('Duration must be a non-empty string');
|
|
48
|
-
// biome-ignore lint/suspicious/noExplicitAny: Testing error cases with invalid types
|
|
49
|
-
expect(() => parseDuration(null)).toThrow('Duration must be a non-empty string');
|
|
50
|
-
// biome-ignore lint/suspicious/noExplicitAny: Testing error cases with invalid types
|
|
51
|
-
expect(() => parseDuration(undefined)).toThrow('Duration must be a non-empty string');
|
|
52
|
-
});
|
|
53
|
-
it('should throw error for invalid format', () => {
|
|
54
|
-
expect(() => parseDuration('2')).toThrow('Use format like "2h", "30m", "2h30m", or "1.5h"');
|
|
55
|
-
expect(() => parseDuration('2hours')).toThrow('Use format like');
|
|
56
|
-
expect(() => parseDuration('2h30')).toThrow('Use format like');
|
|
57
|
-
expect(() => parseDuration('h30m')).toThrow('Use format like');
|
|
58
|
-
expect(() => parseDuration('2x30m')).toThrow('Use format like');
|
|
59
|
-
expect(() => parseDuration('2h30s')).toThrow('Use format like');
|
|
60
|
-
});
|
|
61
|
-
it('should throw error for decimal minutes', () => {
|
|
62
|
-
expect(() => parseDuration('90.5m')).toThrow('Minutes must be a whole number');
|
|
63
|
-
expect(() => parseDuration('1h30.5m')).toThrow('Minutes must be a whole number');
|
|
64
|
-
});
|
|
65
|
-
it('should throw error for negative values', () => {
|
|
66
|
-
expect(() => parseDuration('-2h')).toThrow('Use format like');
|
|
67
|
-
expect(() => parseDuration('-30m')).toThrow('Use format like');
|
|
68
|
-
expect(() => parseDuration('2h-30m')).toThrow('Use format like');
|
|
69
|
-
});
|
|
70
|
-
it('should throw error for zero duration', () => {
|
|
71
|
-
expect(() => parseDuration('0h')).toThrow('Duration must be greater than 0 minutes');
|
|
72
|
-
expect(() => parseDuration('0m')).toThrow('Duration must be greater than 0 minutes');
|
|
73
|
-
expect(() => parseDuration('0h0m')).toThrow('Duration must be greater than 0 minutes');
|
|
74
|
-
});
|
|
75
|
-
it('should throw error for duration exceeding 24 hours', () => {
|
|
76
|
-
expect(() => parseDuration('25h')).toThrow('Duration cannot exceed 24 hours (1440 minutes)');
|
|
77
|
-
expect(() => parseDuration('1441m')).toThrow('Duration cannot exceed 24 hours (1440 minutes)');
|
|
78
|
-
expect(() => parseDuration('24h1m')).toThrow('Duration cannot exceed 24 hours (1440 minutes)');
|
|
79
|
-
expect(() => parseDuration('24.1h')).toThrow('Duration cannot exceed 24 hours (1440 minutes)');
|
|
80
|
-
});
|
|
81
|
-
it('should throw error for malformed numbers', () => {
|
|
82
|
-
expect(() => parseDuration('2.h')).toThrow('Use format like');
|
|
83
|
-
expect(() => parseDuration('2h.m')).toThrow('Use format like');
|
|
84
|
-
expect(() => parseDuration('2..5h')).toThrow('Use format like');
|
|
85
|
-
});
|
|
86
|
-
it('should throw error for duplicate units', () => {
|
|
87
|
-
expect(() => parseDuration('2h3h')).toThrow('Use format like');
|
|
88
|
-
expect(() => parseDuration('30m45m')).toThrow('Use format like');
|
|
89
|
-
});
|
|
90
|
-
it('should throw error for wrong order (minutes before hours)', () => {
|
|
91
|
-
expect(() => parseDuration('30m2h')).toThrow('Use format like');
|
|
92
|
-
expect(() => parseDuration('45m1h')).toThrow('Use format like');
|
|
93
|
-
});
|
|
94
|
-
it('should throw error for invalid mixed formats with correct order', () => {
|
|
95
|
-
expect(() => parseDuration('2h30m15h')).toThrow('Use format like');
|
|
96
|
-
expect(() => parseDuration('1h2h30m')).toThrow('Use format like');
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
|
-
describe('edge cases', () => {
|
|
100
|
-
it('should handle maximum allowed duration', () => {
|
|
101
|
-
expect(parseDuration('24h')).toEqual({ minutes: 1440 });
|
|
102
|
-
expect(parseDuration('1440m')).toEqual({ minutes: 1440 });
|
|
103
|
-
expect(parseDuration('23h60m')).toEqual({ minutes: 1440 });
|
|
104
|
-
});
|
|
105
|
-
it('should handle minimum allowed duration', () => {
|
|
106
|
-
expect(parseDuration('1m')).toEqual({ minutes: 1 });
|
|
107
|
-
expect(parseDuration('0.017h')).toEqual({ minutes: 1 }); // 0.017 * 60 = 1.02 -> 1
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
describe('formatDuration', () => {
|
|
112
|
-
it('should format minutes only', () => {
|
|
113
|
-
expect(formatDuration(45)).toBe('45m');
|
|
114
|
-
expect(formatDuration(1)).toBe('1m');
|
|
115
|
-
expect(formatDuration(59)).toBe('59m');
|
|
116
|
-
});
|
|
117
|
-
it('should format hours only', () => {
|
|
118
|
-
expect(formatDuration(60)).toBe('1h');
|
|
119
|
-
expect(formatDuration(120)).toBe('2h');
|
|
120
|
-
expect(formatDuration(1440)).toBe('24h');
|
|
121
|
-
});
|
|
122
|
-
it('should format hours and minutes combined', () => {
|
|
123
|
-
expect(formatDuration(90)).toBe('1h30m');
|
|
124
|
-
expect(formatDuration(150)).toBe('2h30m');
|
|
125
|
-
expect(formatDuration(105)).toBe('1h45m');
|
|
126
|
-
expect(formatDuration(1439)).toBe('23h59m');
|
|
127
|
-
});
|
|
128
|
-
it('should handle edge cases', () => {
|
|
129
|
-
expect(formatDuration(0)).toBe('0m');
|
|
130
|
-
expect(formatDuration(-5)).toBe('0m');
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
|
-
describe('round trip parsing and formatting', () => {
|
|
134
|
-
const testCases = [
|
|
135
|
-
{ input: '2h', expectedMinutes: 120, expectedFormat: '2h' },
|
|
136
|
-
{ input: '45m', expectedMinutes: 45, expectedFormat: '45m' },
|
|
137
|
-
{ input: '2h30m', expectedMinutes: 150, expectedFormat: '2h30m' },
|
|
138
|
-
{ input: '1.5h', expectedMinutes: 90, expectedFormat: '1h30m' },
|
|
139
|
-
];
|
|
140
|
-
for (const { input, expectedMinutes, expectedFormat } of testCases) {
|
|
141
|
-
it(`should parse "${input}" and format back consistently`, () => {
|
|
142
|
-
const parsed = parseDuration(input);
|
|
143
|
-
expect(parsed.minutes).toBe(expectedMinutes);
|
|
144
|
-
expect(formatDuration(parsed.minutes)).toBe(expectedFormat);
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
});
|
package/dist/utils/labels.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
const LABELS_OPERATORS = ['and', 'or'];
|
|
3
|
-
export const LabelsSchema = {
|
|
4
|
-
labels: z.string().array().optional().describe('The labels to filter the tasks by'),
|
|
5
|
-
labelsOperator: z
|
|
6
|
-
.enum(LABELS_OPERATORS)
|
|
7
|
-
.optional()
|
|
8
|
-
.describe('The operator to use when filtering by labels. This will dictate whether a task has all labels, or some of them. Default is "or".'),
|
|
9
|
-
};
|
|
10
|
-
export function generateLabelsFilter(labels = [], labelsOperator = 'or') {
|
|
11
|
-
if (labels.length === 0)
|
|
12
|
-
return '';
|
|
13
|
-
const operator = labelsOperator === 'and' ? ' & ' : ' | ';
|
|
14
|
-
// Add @ prefix to labels for Todoist API query
|
|
15
|
-
const prefixedLabels = labels.map((label) => (label.startsWith('@') ? label : `@${label}`));
|
|
16
|
-
const labelStr = prefixedLabels.join(` ${operator} `);
|
|
17
|
-
return `(${labelStr})`;
|
|
18
|
-
}
|
package/dist/utils/priorities.js
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
const PRIORITY_VALUES = ['p1', 'p2', 'p3', 'p4'];
|
|
3
|
-
export const PrioritySchema = z.enum(PRIORITY_VALUES, {
|
|
4
|
-
description: 'Task priority: p1 (highest), p2 (high), p3 (medium), p4 (lowest/default)',
|
|
5
|
-
});
|
|
6
|
-
export function convertPriorityToNumber(priority) {
|
|
7
|
-
// Todoist API uses inverse mapping: p1=4 (highest), p2=3, p3=2, p4=1 (lowest)
|
|
8
|
-
const priorityMap = { p1: 4, p2: 3, p3: 2, p4: 1 };
|
|
9
|
-
return priorityMap[priority];
|
|
10
|
-
}
|
|
11
|
-
export function convertNumberToPriority(priority) {
|
|
12
|
-
// Convert Todoist API numbers back to our enum
|
|
13
|
-
const numberMap = { 4: 'p1', 3: 'p2', 2: 'p3', 1: 'p4' };
|
|
14
|
-
return numberMap[priority];
|
|
15
|
-
}
|
|
16
|
-
export function formatPriorityForDisplay(priority) {
|
|
17
|
-
// Convert Todoist API numbers to display format (P1, P2, P3, P4)
|
|
18
|
-
const displayMap = { 4: 'P1', 3: 'P2', 2: 'P3', 1: 'P4' };
|
|
19
|
-
return displayMap[priority] || '';
|
|
20
|
-
}
|
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
import { DisplayLimits } from './constants.js';
|
|
2
|
-
import { formatPriorityForDisplay } from './priorities.js';
|
|
3
|
-
import { ToolNames } from './tool-names.js';
|
|
4
|
-
/**
|
|
5
|
-
* Helper function to get date string in YYYY-MM-DD format
|
|
6
|
-
*/
|
|
7
|
-
export function getDateString(date = new Date()) {
|
|
8
|
-
const parts = date.toISOString().split('T');
|
|
9
|
-
return parts[0] ?? '';
|
|
10
|
-
}
|
|
11
|
-
const { FIND_TASKS_BY_DATE, ADD_TASKS, UPDATE_TASKS, COMPLETE_TASKS, GET_OVERVIEW } = ToolNames;
|
|
12
|
-
/**
|
|
13
|
-
* Creates concise, actionable summaries for task operations instead of raw JSON
|
|
14
|
-
*/
|
|
15
|
-
export function summarizeTaskOperation(action, tasks, options = {}) {
|
|
16
|
-
const { nextSteps, context, showDetails = false } = options;
|
|
17
|
-
const count = tasks.length;
|
|
18
|
-
const bits = [];
|
|
19
|
-
// Main action summary
|
|
20
|
-
const taskOrTasks = count === 1 ? 'task' : 'tasks';
|
|
21
|
-
const actionSummary = `${action} ${count} ${taskOrTasks}${context ? ` ${context}` : ''}.`;
|
|
22
|
-
bits.push(actionSummary);
|
|
23
|
-
// Task details preview (if requested or small batch)
|
|
24
|
-
const smallBatchLimit = 5;
|
|
25
|
-
if (showDetails || count <= smallBatchLimit) {
|
|
26
|
-
const previews = previewTasks(tasks, smallBatchLimit);
|
|
27
|
-
if (previews.length > 0) {
|
|
28
|
-
const moreInfo = count > smallBatchLimit ? `, +${count - smallBatchLimit} more` : '';
|
|
29
|
-
bits.push(`Tasks:\n${previews}${moreInfo}.`);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
// Next steps guidance
|
|
33
|
-
if (nextSteps?.length) {
|
|
34
|
-
bits.push(formatNextSteps(nextSteps));
|
|
35
|
-
}
|
|
36
|
-
return bits.join('\n');
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* Creates batch operation summaries with success/failure breakdown
|
|
40
|
-
*/
|
|
41
|
-
export function summarizeBatch(params) {
|
|
42
|
-
const { action, success, total, successItems, failures, nextSteps } = params;
|
|
43
|
-
const bits = [];
|
|
44
|
-
// Main result summary
|
|
45
|
-
const successBit = `${action}: ${success}/${total} successful.`;
|
|
46
|
-
bits.push(successBit);
|
|
47
|
-
// Success items (if provided and reasonable count)
|
|
48
|
-
if (successItems?.length && successItems.length <= 5) {
|
|
49
|
-
bits.push(`Completed:\n${successItems.map((item) => ` ${item}`).join('\n')}.`);
|
|
50
|
-
}
|
|
51
|
-
// Failure details (if any)
|
|
52
|
-
if (failures?.length) {
|
|
53
|
-
const failureCount = failures.length;
|
|
54
|
-
const failureBit = `Failed (${failureCount}):\n${failures
|
|
55
|
-
.slice(0, DisplayLimits.MAX_FAILURES_SHOWN)
|
|
56
|
-
.map((f) => ` ${f.item} (Error: ${f.error}${f.code ? ` [${f.code}]` : ''})`)
|
|
57
|
-
.join('\n')}${failureCount > DisplayLimits.MAX_FAILURES_SHOWN ? `, +${failureCount - DisplayLimits.MAX_FAILURES_SHOWN} more` : ''}.`;
|
|
58
|
-
bits.push(failureBit);
|
|
59
|
-
}
|
|
60
|
-
// Next steps
|
|
61
|
-
if (nextSteps?.length) {
|
|
62
|
-
bits.push(formatNextSteps(nextSteps));
|
|
63
|
-
}
|
|
64
|
-
return bits.join('\n');
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* Formats a single task-like object into a readable preview line
|
|
68
|
-
*/
|
|
69
|
-
function formatTaskPreview(task) {
|
|
70
|
-
const content = task.content || task.title || 'Untitled';
|
|
71
|
-
const due = task.dueDate ? ` • due ${task.dueDate}` : '';
|
|
72
|
-
const priority = task.priority ? ` • ${formatPriorityForDisplay(task.priority)}` : '';
|
|
73
|
-
const project = task.projectName ? ` • ${task.projectName}` : '';
|
|
74
|
-
const id = task.id ? ` • id=${task.id}` : '';
|
|
75
|
-
return ` ${content}${due}${priority}${project}${id}`;
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* Formats a single project-like object into a readable preview line
|
|
79
|
-
*/
|
|
80
|
-
export function formatProjectPreview(project) {
|
|
81
|
-
const isInbox = project.inboxProject ? ' • Inbox' : '';
|
|
82
|
-
const isFavorite = project.isFavorite ? ' • ⭐' : '';
|
|
83
|
-
const isShared = project.isShared ? ' • Shared' : '';
|
|
84
|
-
const viewStyle = project.viewStyle && project.viewStyle !== 'list' ? ` • ${project.viewStyle}` : '';
|
|
85
|
-
const id = ` • id=${project.id}`;
|
|
86
|
-
return ` ${project.name}${isInbox}${isFavorite}${isShared}${viewStyle}${id}`;
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* Creates preview lines for task lists
|
|
90
|
-
*/
|
|
91
|
-
export function previewTasks(tasks, limit = 5) {
|
|
92
|
-
const previewTasks = tasks.slice(0, limit);
|
|
93
|
-
const lines = previewTasks.map(formatTaskPreview).join('\n');
|
|
94
|
-
// If we're showing fewer tasks than the total, add an indicator
|
|
95
|
-
if (tasks.length > limit) {
|
|
96
|
-
const remaining = tasks.length - limit;
|
|
97
|
-
return `${lines}\n ... and ${remaining} more task${remaining === 1 ? '' : 's'}`;
|
|
98
|
-
}
|
|
99
|
-
return lines;
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Creates list summaries with counts, filters, and guidance
|
|
103
|
-
*/
|
|
104
|
-
export function summarizeList({ subject, count, limit, nextCursor, filterHints, previewLines, zeroReasonHints, nextSteps, }) {
|
|
105
|
-
const bits = [];
|
|
106
|
-
// Header with count and pagination info
|
|
107
|
-
const header = `${subject}: ${count}${typeof limit === 'number' ? ` (limit ${limit})` : ''}${nextCursor ? ', more available' : ''}.`;
|
|
108
|
-
bits.push(header);
|
|
109
|
-
// Filter information
|
|
110
|
-
if (filterHints?.length) {
|
|
111
|
-
bits.push(`Filter: ${filterHints.join('; ')}.`);
|
|
112
|
-
}
|
|
113
|
-
// Preview of items
|
|
114
|
-
if (previewLines?.length) {
|
|
115
|
-
bits.push(`Preview:\n${previewLines}`);
|
|
116
|
-
}
|
|
117
|
-
// Help for empty results
|
|
118
|
-
if (!count && zeroReasonHints?.length) {
|
|
119
|
-
bits.push(`No results. ${zeroReasonHints.join('; ')}.`);
|
|
120
|
-
}
|
|
121
|
-
// Next steps guidance
|
|
122
|
-
if (nextSteps?.length || nextCursor) {
|
|
123
|
-
bits.push(formatNextSteps(nextSteps || [], nextCursor));
|
|
124
|
-
}
|
|
125
|
-
return bits.join('\n');
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* Formats next steps array into a consistent "Next:" section
|
|
129
|
-
* If nextCursor is provided, adds cursor instruction to the steps
|
|
130
|
-
*/
|
|
131
|
-
export function formatNextSteps(nextSteps, nextCursor) {
|
|
132
|
-
const allSteps = [...nextSteps];
|
|
133
|
-
if (nextCursor) {
|
|
134
|
-
allSteps.push(`Pass cursor '${nextCursor}' to fetch more results.`);
|
|
135
|
-
}
|
|
136
|
-
return `Next:\n${allSteps.map((step) => `- ${step}`).join('\n')}`;
|
|
137
|
-
}
|
|
138
|
-
/**
|
|
139
|
-
* Helper to generate contextual next steps based on task operations
|
|
140
|
-
*/
|
|
141
|
-
export function generateTaskNextSteps(operation, tasks, context) {
|
|
142
|
-
const nextSteps = [];
|
|
143
|
-
const count = context?.count ?? tasks.length;
|
|
144
|
-
switch (operation.toLowerCase()) {
|
|
145
|
-
case 'added':
|
|
146
|
-
// Context-aware suggestions for newly added tasks
|
|
147
|
-
if (context?.hasToday) {
|
|
148
|
-
nextSteps.push(`Use ${FIND_TASKS_BY_DATE}('today') to review today's updated schedule`);
|
|
149
|
-
}
|
|
150
|
-
else if (context?.hasOverdue) {
|
|
151
|
-
nextSteps.push(`Use ${FIND_TASKS_BY_DATE}('today') to prioritize past-due items`);
|
|
152
|
-
}
|
|
153
|
-
else if (context?.projectName) {
|
|
154
|
-
nextSteps.push(`Use ${GET_OVERVIEW} with projectId to see ${context.projectName} structure`);
|
|
155
|
-
}
|
|
156
|
-
else {
|
|
157
|
-
nextSteps.push(`Use ${GET_OVERVIEW} to see your updated project organization`);
|
|
158
|
-
}
|
|
159
|
-
// Time-based suggestions
|
|
160
|
-
if (context?.timeOfDay === 'morning') {
|
|
161
|
-
nextSteps.push(`Use ${FIND_TASKS_BY_DATE}('today') to plan your day`);
|
|
162
|
-
}
|
|
163
|
-
break;
|
|
164
|
-
case 'updated':
|
|
165
|
-
case 'organized':
|
|
166
|
-
if (context?.hasToday) {
|
|
167
|
-
nextSteps.push(`Use ${FIND_TASKS_BY_DATE}('today') to see your prioritized schedule`);
|
|
168
|
-
}
|
|
169
|
-
else if (context?.hasHighPriority) {
|
|
170
|
-
nextSteps.push(`Use ${FIND_TASKS_BY_DATE} with filter to focus on high-priority items`);
|
|
171
|
-
}
|
|
172
|
-
else {
|
|
173
|
-
nextSteps.push(`Use ${GET_OVERVIEW} to see your updated project structure`);
|
|
174
|
-
}
|
|
175
|
-
break;
|
|
176
|
-
case 'completed':
|
|
177
|
-
if (context?.timeOfDay === 'evening') {
|
|
178
|
-
nextSteps.push(`Use ${FIND_TASKS_BY_DATE}('tomorrow') to plan upcoming work`);
|
|
179
|
-
}
|
|
180
|
-
else if (context?.hasOverdue) {
|
|
181
|
-
nextSteps.push(`Use ${FIND_TASKS_BY_DATE}('today') to tackle remaining past-due items`);
|
|
182
|
-
}
|
|
183
|
-
else {
|
|
184
|
-
nextSteps.push(`Use ${FIND_TASKS_BY_DATE}('today') to see remaining work`);
|
|
185
|
-
}
|
|
186
|
-
break;
|
|
187
|
-
case 'listed':
|
|
188
|
-
if (context?.isEmptyResult) {
|
|
189
|
-
nextSteps.push(`Use ${ADD_TASKS} to add tasks for this timeframe`);
|
|
190
|
-
nextSteps.push(`Use ${GET_OVERVIEW} with projectId to see tasks in other projects`);
|
|
191
|
-
}
|
|
192
|
-
else if (count > 0) {
|
|
193
|
-
// Tailor suggestions based on result size
|
|
194
|
-
if (count > DisplayLimits.BATCH_OPERATION_THRESHOLD) {
|
|
195
|
-
nextSteps.push(`Use ${UPDATE_TASKS} to batch-update priorities or dates`);
|
|
196
|
-
nextSteps.push('Consider breaking large tasks into subtasks');
|
|
197
|
-
}
|
|
198
|
-
else {
|
|
199
|
-
nextSteps.push(`Use ${UPDATE_TASKS} to modify priorities or due dates`);
|
|
200
|
-
}
|
|
201
|
-
nextSteps.push(`Use ${COMPLETE_TASKS} to mark finished tasks`);
|
|
202
|
-
// Time-sensitive suggestions
|
|
203
|
-
if (context?.hasOverdue) {
|
|
204
|
-
nextSteps.push('Focus on overdue items first to get back on track');
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
break;
|
|
208
|
-
}
|
|
209
|
-
return nextSteps;
|
|
210
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Removes all null fields, empty objects, and empty arrays from an object recursively.
|
|
3
|
-
* This ensures that data sent to agents doesn't include unnecessary empty values.
|
|
4
|
-
*
|
|
5
|
-
* @param obj - The object to sanitize
|
|
6
|
-
* @returns A new object with all null fields, empty objects, and empty arrays removed
|
|
7
|
-
*/
|
|
8
|
-
export function removeNullFields(obj) {
|
|
9
|
-
if (obj === null || obj === undefined) {
|
|
10
|
-
return obj;
|
|
11
|
-
}
|
|
12
|
-
if (Array.isArray(obj)) {
|
|
13
|
-
return obj.map((item) => removeNullFields(item));
|
|
14
|
-
}
|
|
15
|
-
if (typeof obj === 'object') {
|
|
16
|
-
const sanitized = {};
|
|
17
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
18
|
-
if (value !== null) {
|
|
19
|
-
const cleanedValue = removeNullFields(value);
|
|
20
|
-
// Skip empty arrays
|
|
21
|
-
if (Array.isArray(cleanedValue) && cleanedValue.length === 0) {
|
|
22
|
-
continue;
|
|
23
|
-
}
|
|
24
|
-
// Skip empty objects
|
|
25
|
-
if (cleanedValue !== null &&
|
|
26
|
-
typeof cleanedValue === 'object' &&
|
|
27
|
-
!Array.isArray(cleanedValue) &&
|
|
28
|
-
Object.keys(cleanedValue).length === 0) {
|
|
29
|
-
continue;
|
|
30
|
-
}
|
|
31
|
-
sanitized[key] = cleanedValue;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
return sanitized;
|
|
35
|
-
}
|
|
36
|
-
return obj;
|
|
37
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"sanitize-data.test.d.ts","sourceRoot":"","sources":["../../src/utils/sanitize-data.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import { removeNullFields } from './sanitize-data.js';
|
|
2
|
-
describe('removeNullFields', () => {
|
|
3
|
-
it('should remove null fields from objects including nested objects', () => {
|
|
4
|
-
const input = {
|
|
5
|
-
name: 'John',
|
|
6
|
-
age: null,
|
|
7
|
-
email: 'john@example.com',
|
|
8
|
-
phone: null,
|
|
9
|
-
address: {
|
|
10
|
-
street: '123 Main St',
|
|
11
|
-
city: null,
|
|
12
|
-
country: 'USA',
|
|
13
|
-
},
|
|
14
|
-
};
|
|
15
|
-
const result = removeNullFields(input);
|
|
16
|
-
expect(result).toEqual({
|
|
17
|
-
name: 'John',
|
|
18
|
-
email: 'john@example.com',
|
|
19
|
-
address: {
|
|
20
|
-
street: '123 Main St',
|
|
21
|
-
country: 'USA',
|
|
22
|
-
},
|
|
23
|
-
});
|
|
24
|
-
});
|
|
25
|
-
it('should handle arrays with null values', () => {
|
|
26
|
-
const input = {
|
|
27
|
-
items: [
|
|
28
|
-
{ id: 1, value: 'test' },
|
|
29
|
-
{ id: 2, value: null },
|
|
30
|
-
{ id: 3, value: 'another' },
|
|
31
|
-
],
|
|
32
|
-
};
|
|
33
|
-
const result = removeNullFields(input);
|
|
34
|
-
expect(result).toEqual({
|
|
35
|
-
items: [{ id: 1, value: 'test' }, { id: 2 }, { id: 3, value: 'another' }],
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
it('should handle null objects', () => {
|
|
39
|
-
const result = removeNullFields(null);
|
|
40
|
-
expect(result).toBeNull();
|
|
41
|
-
});
|
|
42
|
-
it('should remove empty objects and empty arrays', () => {
|
|
43
|
-
const input = {
|
|
44
|
-
something: 'hello',
|
|
45
|
-
another: {},
|
|
46
|
-
yetAnother: [],
|
|
47
|
-
};
|
|
48
|
-
const result = removeNullFields(input);
|
|
49
|
-
expect(result).toEqual({
|
|
50
|
-
something: 'hello',
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
it('should remove empty objects and empty arrays in nested structures', () => {
|
|
54
|
-
const input = {
|
|
55
|
-
name: 'Test',
|
|
56
|
-
metadata: {},
|
|
57
|
-
tags: [],
|
|
58
|
-
nested: {
|
|
59
|
-
data: 'value',
|
|
60
|
-
emptyObj: {},
|
|
61
|
-
emptyArr: [],
|
|
62
|
-
deepNested: {
|
|
63
|
-
anotherEmpty: {},
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
items: [
|
|
67
|
-
{ id: 1, data: 'test', empty: {} },
|
|
68
|
-
{ id: 2, list: [] },
|
|
69
|
-
],
|
|
70
|
-
};
|
|
71
|
-
const result = removeNullFields(input);
|
|
72
|
-
expect(result).toEqual({
|
|
73
|
-
name: 'Test',
|
|
74
|
-
nested: {
|
|
75
|
-
data: 'value',
|
|
76
|
-
},
|
|
77
|
-
items: [{ id: 1, data: 'test' }, { id: 2 }],
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
it('should keep arrays with values and objects with properties', () => {
|
|
81
|
-
const input = {
|
|
82
|
-
emptyArray: [],
|
|
83
|
-
arrayWithValues: [1, 2, 3],
|
|
84
|
-
emptyObject: {},
|
|
85
|
-
objectWithProps: { key: 'value' },
|
|
86
|
-
};
|
|
87
|
-
const result = removeNullFields(input);
|
|
88
|
-
expect(result).toEqual({
|
|
89
|
-
arrayWithValues: [1, 2, 3],
|
|
90
|
-
objectWithProps: { key: 'value' },
|
|
91
|
-
});
|
|
92
|
-
});
|
|
93
|
-
});
|