@doist/todoist-ai 4.9.3 → 4.9.4
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/mcp-helpers.d.ts.map +1 -1
- package/dist/mcp-helpers.js +5 -2
- package/dist/tools/__tests__/add-projects.test.js +5 -1
- package/dist/tools/__tests__/add-sections.test.js +5 -1
- package/dist/tools/__tests__/assignment-integration.test.js +1 -1
- package/dist/tools/__tests__/complete-tasks.test.js +6 -6
- package/dist/tools/__tests__/find-comments.test.js +3 -9
- package/dist/tools/__tests__/find-projects.test.js +0 -2
- package/dist/tools/__tests__/find-sections.test.js +1 -5
- package/dist/tools/__tests__/find-tasks.test.js +3 -5
- package/dist/tools/__tests__/get-overview.test.js +7 -9
- package/dist/tools/__tests__/update-comments.test.js +0 -2
- package/dist/tools/__tests__/update-projects.test.js +14 -2
- package/dist/tools/__tests__/update-sections.test.js +14 -2
- package/dist/tools/__tests__/update-tasks.test.js +1 -1
- package/dist/utils/sanitize-data.d.ts +9 -0
- package/dist/utils/sanitize-data.d.ts.map +1 -0
- package/dist/utils/sanitize-data.js +37 -0
- package/dist/utils/sanitize-data.test.d.ts +2 -0
- package/dist/utils/sanitize-data.test.d.ts.map +1 -0
- package/dist/utils/sanitize-data.test.js +93 -0
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-helpers.d.ts","sourceRoot":"","sources":["../src/mcp-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAA;AAC/D,OAAO,KAAK,EAAE,SAAS,EAAgB,MAAM,yCAAyC,CAAA;AACtF,OAAO,KAAK,EAAc,CAAC,EAAE,MAAM,KAAK,CAAA;AACxC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;
|
|
1
|
+
{"version":3,"file":"mcp-helpers.d.ts","sourceRoot":"","sources":["../src/mcp-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAA;AAC/D,OAAO,KAAK,EAAE,SAAS,EAAgB,MAAM,yCAAyC,CAAA;AACtF,OAAO,KAAK,EAAc,CAAC,EAAE,MAAM,KAAK,CAAA;AACxC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAmBpD;;;;;;;GAOG;AACH,iBAAS,aAAa,CAAC,iBAAiB,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EACtE,WAAW,EACX,iBAAiB,GACpB,EAAE;IACC,WAAW,EAAE,MAAM,CAAA;IACnB,iBAAiB,EAAE,iBAAiB,CAAA;CACvC;;;;;;;;;;;;;;;;;EAkBA;AASD;;;;;GAKG;AACH,iBAAS,YAAY,CAAC,MAAM,SAAS,CAAC,CAAC,WAAW,EAC9C,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,EACzB,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,UAAU,QAqBrB;AAED,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,CAAA"}
|
package/dist/mcp-helpers.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { removeNullFields } from './utils/sanitize-data.js';
|
|
1
2
|
/**
|
|
2
3
|
* Wether to return the structured content directly, vs. in the `content` part of the output.
|
|
3
4
|
*
|
|
@@ -21,13 +22,15 @@ const USE_STRUCTURED_CONTENT = process.env.USE_STRUCTURED_CONTENT === 'true' ||
|
|
|
21
22
|
* @see USE_STRUCTURED_CONTENT - Wether to use the structured content feature of the MCP protocol.
|
|
22
23
|
*/
|
|
23
24
|
function getToolOutput({ textContent, structuredContent, }) {
|
|
25
|
+
// Remove null fields from structured content before returning
|
|
26
|
+
const sanitizedContent = removeNullFields(structuredContent);
|
|
24
27
|
if (USE_STRUCTURED_CONTENT) {
|
|
25
28
|
return {
|
|
26
29
|
content: [{ type: 'text', text: textContent }],
|
|
27
|
-
structuredContent,
|
|
30
|
+
structuredContent: sanitizedContent,
|
|
28
31
|
};
|
|
29
32
|
}
|
|
30
|
-
const json = JSON.stringify(
|
|
33
|
+
const json = JSON.stringify(sanitizedContent);
|
|
31
34
|
return {
|
|
32
35
|
content: [
|
|
33
36
|
{ type: 'text', text: textContent },
|
|
@@ -143,7 +143,11 @@ describe(`${ADD_PROJECTS} tool`, () => {
|
|
|
143
143
|
// Verify structured content
|
|
144
144
|
const structuredContent = extractStructuredContent(result);
|
|
145
145
|
expect(structuredContent).toEqual(expect.objectContaining({
|
|
146
|
-
projects:
|
|
146
|
+
projects: expect.arrayContaining([
|
|
147
|
+
expect.objectContaining({ id: 'project-1', name: 'First Project' }),
|
|
148
|
+
expect.objectContaining({ id: 'project-2', name: 'Second Project' }),
|
|
149
|
+
expect.objectContaining({ id: 'project-3', name: 'Third Project' }),
|
|
150
|
+
]),
|
|
147
151
|
totalCount: 3,
|
|
148
152
|
}));
|
|
149
153
|
});
|
|
@@ -122,7 +122,11 @@ describe(`${ADD_SECTIONS} tool`, () => {
|
|
|
122
122
|
// Verify structured content
|
|
123
123
|
const structuredContent = extractStructuredContent(result);
|
|
124
124
|
expect(structuredContent).toEqual(expect.objectContaining({
|
|
125
|
-
sections:
|
|
125
|
+
sections: expect.arrayContaining([
|
|
126
|
+
expect.objectContaining({ id: 'section-1', name: 'First Section' }),
|
|
127
|
+
expect.objectContaining({ id: 'section-2', name: 'Second Section' }),
|
|
128
|
+
expect.objectContaining({ id: 'section-3', name: 'Third Section' }),
|
|
129
|
+
]),
|
|
126
130
|
totalCount: 3,
|
|
127
131
|
}));
|
|
128
132
|
});
|
|
@@ -311,7 +311,7 @@ describe('Assignment Integration Tests', () => {
|
|
|
311
311
|
projectId: 'project-123',
|
|
312
312
|
}, mockTodoistApi);
|
|
313
313
|
expect(extractTextContent(result)).toContain('is not shared and has no collaborators');
|
|
314
|
-
expect(extractStructuredContent(result).collaborators).
|
|
314
|
+
expect(extractStructuredContent(result).collaborators).toBeUndefined(); // Empty arrays are removed
|
|
315
315
|
});
|
|
316
316
|
it('should handle project not found', async () => {
|
|
317
317
|
mockTodoistApi.getProject.mockRejectedValueOnce(new Error('Project not found'));
|
|
@@ -24,13 +24,13 @@ describe(`${COMPLETE_TASKS} tool`, () => {
|
|
|
24
24
|
expect(extractTextContent(result)).toMatchSnapshot();
|
|
25
25
|
// Verify structured content
|
|
26
26
|
const { structuredContent } = result;
|
|
27
|
-
expect(structuredContent).toEqual(
|
|
27
|
+
expect(structuredContent).toEqual({
|
|
28
28
|
completed: ['task-1', 'task-2', 'task-3'],
|
|
29
|
-
failures
|
|
29
|
+
// failures array is removed when empty
|
|
30
30
|
totalRequested: 3,
|
|
31
31
|
successCount: 3,
|
|
32
32
|
failureCount: 0,
|
|
33
|
-
})
|
|
33
|
+
});
|
|
34
34
|
});
|
|
35
35
|
it('should complete single task', async () => {
|
|
36
36
|
mockTodoistApi.closeTask.mockResolvedValue(true);
|
|
@@ -40,13 +40,13 @@ describe(`${COMPLETE_TASKS} tool`, () => {
|
|
|
40
40
|
expect(extractTextContent(result)).toMatchSnapshot();
|
|
41
41
|
// Verify structured content
|
|
42
42
|
const { structuredContent } = result;
|
|
43
|
-
expect(structuredContent).toEqual(
|
|
43
|
+
expect(structuredContent).toEqual({
|
|
44
44
|
completed: ['8485093748'],
|
|
45
|
-
failures
|
|
45
|
+
// failures array is removed when empty
|
|
46
46
|
totalRequested: 1,
|
|
47
47
|
successCount: 1,
|
|
48
48
|
failureCount: 0,
|
|
49
|
-
})
|
|
49
|
+
});
|
|
50
50
|
});
|
|
51
51
|
it('should handle partial failures gracefully', async () => {
|
|
52
52
|
// Mock first and third tasks to succeed, second to fail
|
|
@@ -55,7 +55,6 @@ describe(`${FIND_COMMENTS} tool`, () => {
|
|
|
55
55
|
searchType: 'task',
|
|
56
56
|
searchId: 'task123',
|
|
57
57
|
hasMore: false,
|
|
58
|
-
nextCursor: null,
|
|
59
58
|
totalCount: 2,
|
|
60
59
|
}));
|
|
61
60
|
});
|
|
@@ -128,7 +127,6 @@ describe(`${FIND_COMMENTS} tool`, () => {
|
|
|
128
127
|
searchType: 'project',
|
|
129
128
|
searchId: 'project456',
|
|
130
129
|
hasMore: false,
|
|
131
|
-
nextCursor: null,
|
|
132
130
|
totalCount: 1,
|
|
133
131
|
}));
|
|
134
132
|
});
|
|
@@ -155,13 +153,11 @@ describe(`${FIND_COMMENTS} tool`, () => {
|
|
|
155
153
|
id: 'comment789',
|
|
156
154
|
content: 'Single comment content',
|
|
157
155
|
taskId: 'task123',
|
|
158
|
-
fileAttachment: null,
|
|
159
156
|
}),
|
|
160
157
|
]),
|
|
161
158
|
searchType: 'single',
|
|
162
159
|
searchId: 'comment789',
|
|
163
160
|
hasMore: false,
|
|
164
|
-
nextCursor: null,
|
|
165
161
|
totalCount: 1,
|
|
166
162
|
}));
|
|
167
163
|
});
|
|
@@ -200,7 +196,6 @@ describe(`${FIND_COMMENTS} tool`, () => {
|
|
|
200
196
|
searchType: 'single',
|
|
201
197
|
searchId: 'comment789',
|
|
202
198
|
hasMore: false,
|
|
203
|
-
nextCursor: null,
|
|
204
199
|
totalCount: 1,
|
|
205
200
|
}));
|
|
206
201
|
});
|
|
@@ -229,14 +224,13 @@ describe(`${FIND_COMMENTS} tool`, () => {
|
|
|
229
224
|
expect(extractTextContent(result)).toMatchSnapshot();
|
|
230
225
|
// Verify structured content
|
|
231
226
|
const structuredContent = extractStructuredContent(result);
|
|
232
|
-
expect(structuredContent).toEqual(
|
|
233
|
-
comments: [],
|
|
227
|
+
expect(structuredContent).toEqual({
|
|
234
228
|
searchType: 'task',
|
|
235
229
|
searchId: 'task123',
|
|
236
230
|
hasMore: false,
|
|
237
|
-
nextCursor: null,
|
|
238
231
|
totalCount: 0,
|
|
239
|
-
|
|
232
|
+
// comments array is removed when empty
|
|
233
|
+
});
|
|
240
234
|
});
|
|
241
235
|
});
|
|
242
236
|
});
|
|
@@ -53,7 +53,6 @@ describe(`${FIND_PROJECTS} tool`, () => {
|
|
|
53
53
|
projects: expect.any(Array),
|
|
54
54
|
totalCount: 3,
|
|
55
55
|
hasMore: false,
|
|
56
|
-
nextCursor: null,
|
|
57
56
|
appliedFilters: {
|
|
58
57
|
search: undefined,
|
|
59
58
|
limit: 50,
|
|
@@ -112,7 +111,6 @@ describe(`${FIND_PROJECTS} tool`, () => {
|
|
|
112
111
|
expect(structuredContent.projects).toHaveLength(2); // Should match filtered results
|
|
113
112
|
expect(structuredContent.totalCount).toBe(2);
|
|
114
113
|
expect(structuredContent.hasMore).toBe(false);
|
|
115
|
-
expect(structuredContent.nextCursor).toBeNull();
|
|
116
114
|
expect(structuredContent.appliedFilters).toEqual({
|
|
117
115
|
search: 'work',
|
|
118
116
|
limit: 50,
|
|
@@ -77,12 +77,8 @@ describe(`${FIND_SECTIONS} tool`, () => {
|
|
|
77
77
|
expect(textContent).toContain(`Use ${ADD_SECTIONS} to create sections`);
|
|
78
78
|
// Verify structured content
|
|
79
79
|
const structuredContent = extractStructuredContent(result);
|
|
80
|
-
expect(structuredContent.sections).
|
|
80
|
+
expect(structuredContent.sections).toBeUndefined(); // Empty arrays are removed
|
|
81
81
|
expect(structuredContent.totalCount).toBe(0);
|
|
82
|
-
expect(structuredContent.appliedFilters).toEqual({
|
|
83
|
-
projectId: 'empty-project-id',
|
|
84
|
-
search: undefined,
|
|
85
|
-
});
|
|
86
82
|
});
|
|
87
83
|
});
|
|
88
84
|
describe('searching sections by name', () => {
|
|
@@ -117,7 +117,6 @@ describe(`${FIND_TASKS} tool`, () => {
|
|
|
117
117
|
tasks: expect.any(Array),
|
|
118
118
|
totalCount: 1,
|
|
119
119
|
hasMore: false,
|
|
120
|
-
nextCursor: null,
|
|
121
120
|
appliedFilters: expect.objectContaining({
|
|
122
121
|
searchText: params.searchText,
|
|
123
122
|
limit: expectedLimit,
|
|
@@ -144,15 +143,14 @@ describe(`${FIND_TASKS} tool`, () => {
|
|
|
144
143
|
expect(extractTextContent(result)).toMatchSnapshot();
|
|
145
144
|
// Verify structured content for empty results
|
|
146
145
|
const structuredContent = extractStructuredContent(result);
|
|
147
|
-
expect(structuredContent).toEqual(
|
|
148
|
-
tasks
|
|
146
|
+
expect(structuredContent).toEqual({
|
|
147
|
+
// tasks array is removed when empty
|
|
149
148
|
totalCount: 0,
|
|
150
149
|
hasMore: false,
|
|
151
|
-
nextCursor: null,
|
|
152
150
|
appliedFilters: expect.objectContaining({
|
|
153
151
|
searchText: searchText,
|
|
154
152
|
}),
|
|
155
|
-
})
|
|
153
|
+
});
|
|
156
154
|
});
|
|
157
155
|
});
|
|
158
156
|
describe('validation', () => {
|
|
@@ -59,7 +59,7 @@ describe(`${GET_OVERVIEW} tool`, () => {
|
|
|
59
59
|
inbox: expect.objectContaining({
|
|
60
60
|
id: TEST_IDS.PROJECT_INBOX,
|
|
61
61
|
name: 'Inbox',
|
|
62
|
-
sections
|
|
62
|
+
// sections array removed if empty
|
|
63
63
|
}),
|
|
64
64
|
projects: expect.any(Array),
|
|
65
65
|
totalProjects: 2,
|
|
@@ -75,14 +75,13 @@ describe(`${GET_OVERVIEW} tool`, () => {
|
|
|
75
75
|
expect(extractTextContent(result)).toMatchSnapshot();
|
|
76
76
|
// Test structured content sanity checks
|
|
77
77
|
const structuredContent = extractStructuredContent(result);
|
|
78
|
-
expect(structuredContent).toEqual(
|
|
78
|
+
expect(structuredContent).toEqual({
|
|
79
79
|
type: 'account_overview',
|
|
80
|
-
|
|
81
|
-
projects: [],
|
|
80
|
+
// projects array is removed when empty
|
|
82
81
|
totalProjects: 0,
|
|
83
82
|
totalSections: 0,
|
|
84
83
|
hasNestedProjects: false,
|
|
85
|
-
})
|
|
84
|
+
});
|
|
86
85
|
});
|
|
87
86
|
});
|
|
88
87
|
describe('project overview (with projectId)', () => {
|
|
@@ -188,20 +187,19 @@ describe(`${GET_OVERVIEW} tool`, () => {
|
|
|
188
187
|
expect(extractTextContent(result)).toMatchSnapshot();
|
|
189
188
|
// Test structured content sanity checks
|
|
190
189
|
const structuredContent = extractStructuredContent(result);
|
|
191
|
-
expect(structuredContent).toEqual(
|
|
190
|
+
expect(structuredContent).toEqual({
|
|
192
191
|
type: 'project_overview',
|
|
193
192
|
project: expect.objectContaining({
|
|
194
193
|
id: 'empty-project-id',
|
|
195
194
|
name: 'Empty Project',
|
|
196
195
|
}),
|
|
197
|
-
sections
|
|
198
|
-
tasks: [],
|
|
196
|
+
// sections and tasks arrays are removed when empty
|
|
199
197
|
stats: expect.objectContaining({
|
|
200
198
|
totalTasks: 0,
|
|
201
199
|
totalSections: 0,
|
|
202
200
|
tasksWithoutSection: 0,
|
|
203
201
|
}),
|
|
204
|
-
})
|
|
202
|
+
});
|
|
205
203
|
});
|
|
206
204
|
});
|
|
207
205
|
describe('error handling', () => {
|
|
@@ -52,7 +52,6 @@ describe(`${UPDATE_COMMENTS} tool`, () => {
|
|
|
52
52
|
id: '98765',
|
|
53
53
|
content: 'Updated content here',
|
|
54
54
|
taskId: 'task456',
|
|
55
|
-
fileAttachment: null,
|
|
56
55
|
}),
|
|
57
56
|
],
|
|
58
57
|
totalCount: 1,
|
|
@@ -89,7 +88,6 @@ describe(`${UPDATE_COMMENTS} tool`, () => {
|
|
|
89
88
|
content: 'Updated project comment',
|
|
90
89
|
taskId: undefined,
|
|
91
90
|
projectId: 'project789',
|
|
92
|
-
fileAttachment: null,
|
|
93
91
|
}),
|
|
94
92
|
],
|
|
95
93
|
totalCount: 1,
|
|
@@ -47,7 +47,12 @@ describe(`${UPDATE_PROJECTS} tool`, () => {
|
|
|
47
47
|
// Verify structured content
|
|
48
48
|
const structuredContent = extractStructuredContent(result);
|
|
49
49
|
expect(structuredContent).toEqual(expect.objectContaining({
|
|
50
|
-
projects: [
|
|
50
|
+
projects: expect.arrayContaining([
|
|
51
|
+
expect.objectContaining({
|
|
52
|
+
id: 'existing-project-123',
|
|
53
|
+
name: 'Updated Project Name',
|
|
54
|
+
}),
|
|
55
|
+
]),
|
|
51
56
|
totalCount: 1,
|
|
52
57
|
updatedProjectIds: ['existing-project-123'],
|
|
53
58
|
appliedOperations: {
|
|
@@ -140,7 +145,14 @@ describe(`${UPDATE_PROJECTS} tool`, () => {
|
|
|
140
145
|
// Verify structured content
|
|
141
146
|
const structuredContent = extractStructuredContent(result);
|
|
142
147
|
expect(structuredContent).toEqual(expect.objectContaining({
|
|
143
|
-
projects:
|
|
148
|
+
projects: expect.arrayContaining([
|
|
149
|
+
expect.objectContaining({ id: 'project-1', name: 'Updated First Project' }),
|
|
150
|
+
expect.objectContaining({
|
|
151
|
+
id: 'project-2',
|
|
152
|
+
name: 'Updated Second Project',
|
|
153
|
+
}),
|
|
154
|
+
expect.objectContaining({ id: 'project-3', name: 'Updated Third Project' }),
|
|
155
|
+
]),
|
|
144
156
|
totalCount: 3,
|
|
145
157
|
updatedProjectIds: ['project-1', 'project-2', 'project-3'],
|
|
146
158
|
appliedOperations: {
|
|
@@ -40,7 +40,12 @@ describe(`${UPDATE_SECTIONS} tool`, () => {
|
|
|
40
40
|
// Verify structured content
|
|
41
41
|
const structuredContent = extractStructuredContent(result);
|
|
42
42
|
expect(structuredContent).toEqual(expect.objectContaining({
|
|
43
|
-
sections: [
|
|
43
|
+
sections: expect.arrayContaining([
|
|
44
|
+
expect.objectContaining({
|
|
45
|
+
id: 'existing-section-123',
|
|
46
|
+
name: 'Updated Section Name',
|
|
47
|
+
}),
|
|
48
|
+
]),
|
|
44
49
|
totalCount: 1,
|
|
45
50
|
updatedSectionIds: ['existing-section-123'],
|
|
46
51
|
}));
|
|
@@ -97,7 +102,14 @@ describe(`${UPDATE_SECTIONS} tool`, () => {
|
|
|
97
102
|
// Verify structured content
|
|
98
103
|
const structuredContent = extractStructuredContent(result);
|
|
99
104
|
expect(structuredContent).toEqual(expect.objectContaining({
|
|
100
|
-
sections:
|
|
105
|
+
sections: expect.arrayContaining([
|
|
106
|
+
expect.objectContaining({ id: 'section-1', name: 'Updated First Section' }),
|
|
107
|
+
expect.objectContaining({
|
|
108
|
+
id: 'section-2',
|
|
109
|
+
name: 'Updated Second Section',
|
|
110
|
+
}),
|
|
111
|
+
expect.objectContaining({ id: 'section-3', name: 'Updated Third Section' }),
|
|
112
|
+
]),
|
|
101
113
|
totalCount: 3,
|
|
102
114
|
updatedSectionIds: ['section-1', 'section-2', 'section-3'],
|
|
103
115
|
}));
|
|
@@ -673,7 +673,7 @@ describe(`${UPDATE_TASKS} tool`, () => {
|
|
|
673
673
|
const textContent = extractTextContent(result);
|
|
674
674
|
expect(textContent).toContain('Updated 0 tasks');
|
|
675
675
|
const structuredContent = extractStructuredContent(result);
|
|
676
|
-
expect(structuredContent.tasks).
|
|
676
|
+
expect(structuredContent.tasks).toBeUndefined(); // Empty arrays are removed
|
|
677
677
|
expect(structuredContent.totalCount).toBe(0);
|
|
678
678
|
});
|
|
679
679
|
});
|
|
@@ -0,0 +1,9 @@
|
|
|
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 declare function removeNullFields<T>(obj: T): T;
|
|
9
|
+
//# sourceMappingURL=sanitize-data.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sanitize-data.d.ts","sourceRoot":"","sources":["../../src/utils/sanitize-data.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAqC7C"}
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sanitize-data.test.d.ts","sourceRoot":"","sources":["../../src/utils/sanitize-data.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,93 @@
|
|
|
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
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@doist/todoist-ai",
|
|
3
|
-
"version": "4.9.
|
|
3
|
+
"version": "4.9.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"zod": "^3.25.7"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
|
-
"@biomejs/biome": "2.2.
|
|
55
|
+
"@biomejs/biome": "2.2.5",
|
|
56
56
|
"@types/express": "^5.0.2",
|
|
57
57
|
"@types/jest": "30.0.0",
|
|
58
58
|
"@types/morgan": "^1.9.9",
|