Skip to content

feat(Todoist Node): Add more resources and operations #17925

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 43 commits into
base: master
Choose a base branch
from

Conversation

mutdmour
Copy link
Contributor

@mutdmour mutdmour commented Aug 2, 2025

Summary

Add more resources and operations to Todoist node

Screen.Recording.2025-08-03.at.15.40.57.mov

Related Linear tickets, Github issues, and Community forum posts

Review / Merge checklist

  • PR title and summary are descriptive. (conventions)
  • Docs updated or follow-up ticket created.
  • Tests included.
  • PR Labeled with release/backport (if the PR is an urgent fix that needs to be backported)

@n8n-assistant n8n-assistant bot added the n8n team Authored by the n8n team label Aug 2, 2025
@n8n-assistant n8n-assistant bot added the node/improvement New feature or request label Aug 2, 2025
mutdmour and others added 14 commits August 2, 2025 11:52
- Split monolithic function into focused, single-responsibility functions
- Added assertIsValidObject, assertIsRequiredParameter, assertIsResourceLocator
- Added assertIsObjectType, assertIsArrayType, assertIsPrimitiveType helpers
- Added validateParameterType and validateParameterAgainstTypes coordinators
- Reused existing assertIsArray function to reduce code duplication
- Maintained exact same functionality while improving maintainability
- All existing tests pass

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
@mutdmour mutdmour marked this pull request as ready for review August 3, 2025 13:51
@mutdmour
Copy link
Contributor Author

mutdmour commented Aug 3, 2025

@claude review

Copy link

claude bot commented Aug 3, 2025

Claude finished @mutdmour's task —— View job


Review Complete ✅

  • Read repository guidelines (CLAUDE.md)
  • Review main implementation files
  • Examine test coverage and quality
  • Check utility type changes
  • Provide comprehensive review feedback

Code Review Summary

This PR significantly expands the Todoist node by adding support for Projects, Sections, Comments, and Labels resources, along with comprehensive operations for each. The implementation follows good architectural patterns with proper separation of concerns.

Strengths

Architecture & Design:

  • Clean separation of concerns with Service.ts orchestrating OperationHandler.ts classes
  • Consistent handler pattern across all resource types
  • Type-safe operation validation using const arrays and type guards (isProjectOperationType, etc.)
  • Proper use of TypeScript strict typing throughout

Implementation Quality:

  • Comprehensive parameter validation using the new assertIsNodeParameters utility
  • Consistent error handling and response patterns
  • Proper handling of both success and data responses
  • Good use of TypeScript assertion functions for runtime type safety

Test Coverage:

  • Extensive unit tests covering all handlers (1,232 lines in OperationHandler.test.ts)
  • Comprehensive workflow tests demonstrating real-world usage scenarios
  • Good mock setup and test organization
  • New utility type tests ensuring robustness

User Experience:

  • Rich node definition with proper resource locators and dynamic options
  • Comprehensive field definitions with proper validation constraints
  • Good use of display conditions to show/hide relevant fields
  • Helpful descriptions and placeholders throughout

🔍 Areas for Improvement

Minor Issues:

  1. Duplicate Field Definition (TodoistV2.node.ts:922-927):

    // Duplicate due_lang field in updateFields
    {
      displayName: 'Due String Locale',
      name: 'dueLang', // This appears twice
    }
  2. Missing Null Checks (OperationHandler.ts:332):

    // Could be more defensive about null values
    return new Map(sections.map((s) => [s.name, s.id as unknown as number]));
  3. Type Assertion (OperationHandler.ts:332):
    The as unknown as number cast could be avoided with proper typing of the Section interface.

Suggestions:

  1. Consistent Response Handling: Consider standardizing the response format across all handlers - some return { data }, others { success: true }.

  2. Error Context: The error messages in type assertions could include more context about expected vs actual values.

  3. Code Comments: While the code is self-documenting, some complex operations (like the sync command processing) could benefit from inline documentation.

🛡️ Security & Best Practices

  • ✅ Proper input validation throughout
  • ✅ No hardcoded credentials or sensitive data
  • ✅ Appropriate use of TypeScript strict mode
  • ✅ Good separation between API calls and business logic
  • ✅ Proper error handling without information leakage

📊 Test Quality Assessment

The test suite is comprehensive and well-structured:

  • Coverage: All handlers are tested with multiple scenarios
  • Mocking: Appropriate mocking of external dependencies
  • Organization: Clear test structure with good describe/it blocks
  • Assertions: Proper verification of both successful and error cases

🎯 Recommendation

✅ APPROVE - This is a high-quality implementation that significantly enhances the Todoist node's functionality. The code follows n8n's patterns well, includes comprehensive tests, and provides a great user experience. The minor issues identified above are not blocking and can be addressed in follow-up improvements.

The PR successfully delivers on its promise to "add more resources and operations" with professional-grade implementation quality.

@mutdmour mutdmour requested a review from Joffcom August 3, 2025 13:52
Comment on lines +259 to +260
const id = ctx.getNodeParameter('taskId', itemIndex);
assertIsStringOrNumber('taskId', id);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe these could be merged into its own function, but I have decided this is out of scope here.

const commandsJson = ctx.getNodeParameter('commands', itemIndex) as string;
const commandsJson = ctx.getNodeParameter('commands', itemIndex);
assertIsString('commands', commandsJson);

const projectId = ctx.getNodeParameter('project', itemIndex, undefined, {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this one is weird. Does not seem to be used. Maybe we should remove this? Thoughts @Joffcom ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mutdmour 🤔 Yeah if we are not using it we might as well remove it, I wonder where it came from

project_id?: number | string;
section?: string;
content?: string;
};
}

export const CommandTypes = {
// Item/Task commands
ITEM_MOVE: 'item_move',
ITEM_ADD: 'item_add',
ITEM_UPDATE: 'item_update',
ITEM_REORDER: 'item_reorder',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this has unused command types, so I have decided to complete it for sake completeness. Or should we clean this up?

description: 'The content of the note',
},
{
displayName: 'Reminder',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this one is weird. adding reminder does not always work for me. But works sometimes. Not sure if it's a limitation on my account or an issue with the API. Will leave it since it kinda works.

I created changes with a "Reminders" resource, but stashed them for now. Can create a separate PR for that once we understand why it does not work consistently.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cubic analysis

8 issues found across 9 files • Review in cubic

React with 👍 or 👎 to teach cubic. You can also tag @cubic-dev-ai to give feedback, ask questions, or re-run the review.


// Task operations
todoistNock.post('/rest/v2/tasks').reply(200, taskData);
todoistNock.post('/rest/v2/tasks').reply(200, taskData2);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same endpoint is mocked twice in succession; while nock will match them in order, this pattern is brittle and obscures intent. Use .times(2) or chain .reply() with an array to make it explicit that two calls are expected.

Prompt for AI agents
Address the following comment on packages/nodes-base/nodes/Todoist/v2/test/TodoistV2.node.test.ts at line 277:

<comment>The same endpoint is mocked twice in succession; while nock will match them in order, this pattern is brittle and obscures intent. Use `.times(2)` or chain `.reply()` with an array to make it explicit that two calls are expected.</comment>

<file context>
@@ -0,0 +1,347 @@
+import { NodeTestHarness } from &#39;@nodes-testing/node-test-harness&#39;;
+import type { WorkflowTestData } from &#39;n8n-workflow&#39;;
+import nock from &#39;nock&#39;;
+
+// Mock data with randomized IDs and generic names
+const projectData = {
+	id: &#39;1234567890&#39;,
+	parent_id: null,
+	order: 31,
</file context>


const successResponse = { success: true };

describe('Execute TodoistV2 Node', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HTTP mocks created with nock are not cleaned up, which can lead to test leakage and flakiness when more tests are introduced. Add an afterEach that calls nock.cleanAll() (Based on your team's feedback about improving test reliability).

Prompt for AI agents
Address the following comment on packages/nodes-base/nodes/Todoist/v2/test/TodoistV2.node.test.ts at line 249:

<comment>HTTP mocks created with nock are not cleaned up, which can lead to test leakage and flakiness when more tests are introduced. Add an afterEach that calls nock.cleanAll() (Based on your team&#39;s feedback about improving test reliability).</comment>

<file context>
@@ -0,0 +1,347 @@
+import { NodeTestHarness } from &#39;@nodes-testing/node-test-harness&#39;;
+import type { WorkflowTestData } from &#39;n8n-workflow&#39;;
+import nock from &#39;nock&#39;;
+
+// Mock data with randomized IDs and generic names
+const projectData = {
+	id: &#39;1234567890&#39;,
+	parent_id: null,
+	order: 31,
</file context>

}

function createElementValidator(elementType: 'string' | 'number' | 'boolean') {
return (val: unknown): val is string | number | boolean => typeof val === elementType;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type predicate declares that the validator narrows to string | number | boolean independent of the requested elementType, so an array validated for string will still be typed as (string | number | boolean)[]. This weakens compile-time guarantees produced by assertIsArray() and can let type mismatches slip past the compiler. Consider returning a predicate whose narrowing matches the specific elementType that was passed in. (Based on your team's feedback about maintaining accurate TypeScript typings for safer code.)

Prompt for AI agents
Address the following comment on packages/nodes-base/utils/types.ts at line 84:

<comment>The type predicate declares that the validator narrows to `string | number | boolean` independent of the requested `elementType`, so an array validated for `string` will still be typed as `(string | number | boolean)[]`. This weakens compile-time guarantees produced by `assertIsArray()` and can let type mismatches slip past the compiler. Consider returning a predicate whose narrowing matches the specific `elementType` that was passed in. (Based on your team&#39;s feedback about maintaining accurate TypeScript typings for safer code.)</comment>

<file context>
@@ -28,67 +38,130 @@ export function assertIsArray&lt;T&gt;(
 	);
 }
 
+type ParameterType =
+	| &#39;string&#39;
+	| &#39;boolean&#39;
+	| &#39;number&#39;
+	| &#39;resource-locator&#39;
+	| &#39;string[]&#39;
</file context>


// Mock uuid
jest.mock('uuid', () => ({
v4: jest.fn(() => 'mock-uuid-123'),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mocking uuid.v4 with a constant value removes the uniqueness guarantee and can hide bugs related to duplicate IDs in logic that depends on distinct UUIDs (e.g. temp_id vs uuid generation in SyncHandler). Consider returning deterministically unique values for each call instead.

Prompt for AI agents
Address the following comment on packages/nodes-base/nodes/Todoist/v2/test/OperationHandler.test.ts at line 50:

<comment>Mocking uuid.v4 with a constant value removes the uniqueness guarantee and can hide bugs related to duplicate IDs in logic that depends on distinct UUIDs (e.g. temp_id vs uuid generation in SyncHandler). Consider returning deterministically unique values for each call instead.</comment>

<file context>
@@ -0,0 +1,1232 @@
+import { mock } from &#39;jest-mock-extended&#39;;
+import type { IExecuteFunctions, INode } from &#39;n8n-workflow&#39;;
+
+import { todoistApiRequest, todoistSyncRequest } from &#39;../../GenericFunctions&#39;;
+import {
+	CreateHandler,
+	CloseHandler,
+	DeleteHandler,
+	GetHandler,
</file context>

// Mock Context interface
const createMockContext = (params: Record<string, any> = {}) =>
mock<IExecuteFunctions>({
getNodeParameter: jest.fn((key: string) => params[key]),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mocked getNodeParameter ignores the optional default-value and options arguments that the production code frequently relies on (e.g. getNodeParameter('options', idx, {})). Tests will therefore return undefined instead of the supplied default when a key is missing, potentially letting incorrect logic pass unnoticed and reducing test accuracy.

Prompt for AI agents
Address the following comment on packages/nodes-base/nodes/Todoist/v2/test/OperationHandler.test.ts at line 59:

<comment>The mocked getNodeParameter ignores the optional default-value and options arguments that the production code frequently relies on (e.g. getNodeParameter(&#39;options&#39;, idx, {})). Tests will therefore return undefined instead of the supplied default when a key is missing, potentially letting incorrect logic pass unnoticed and reducing test accuracy.</comment>

<file context>
@@ -0,0 +1,1232 @@
+import { mock } from &#39;jest-mock-extended&#39;;
+import type { IExecuteFunctions, INode } from &#39;n8n-workflow&#39;;
+
+import { todoistApiRequest, todoistSyncRequest } from &#39;../../GenericFunctions&#39;;
+import {
+	CreateHandler,
+	CloseHandler,
+	DeleteHandler,
+	GetHandler,
</file context>
Suggested change
getNodeParameter: jest.fn((key: string) => params[key]),
getNodeParameter: jest.fn((key: string, _idx?: number, defaultValue?: any) => (key in params ? params[key] : defaultValue)),

): Promise<any> {
const authentication = this.getNodeParameter('authentication', 0, 'oAuth2');

const options: IRequestOptions = {
headers: {},
method: 'POST',
qs,
uri: 'https://api.todoist.com/sync/v9/sync',
uri: `https://api.todoist.com/sync/v9${endpoint}`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a caller supplies an endpoint value that does not begin with a leading slash (e.g. "projects"), the concatenation will generate an invalid URL such as "https://api.todoist.com/sync/v9projects". This brittleness can be avoided by defensively adding the slash when it is missing.

Prompt for AI agents
Address the following comment on packages/nodes-base/nodes/Todoist/GenericFunctions.ts at line 62:

<comment>If a caller supplies an endpoint value that does not begin with a leading slash (e.g. &quot;projects&quot;), the concatenation will generate an invalid URL such as &quot;https://api.todoist.com/sync/v9projects&quot;. This brittleness can be avoided by defensively adding the slash when it is missing.</comment>

<file context>
@@ -51,14 +51,15 @@ export async function todoistSyncRequest(
 	this: Context,
 	body: any = {},
 	qs: IDataObject = {},
+	endpoint: string = &#39;/sync&#39;,
 ): Promise&lt;any&gt; {
 	const authentication = this.getNodeParameter(&#39;authentication&#39;, 0, &#39;oAuth2&#39;);
 
 	const options: IRequestOptions = {
 		headers: {},
</file context>
Suggested change
uri: `https://api.todoist.com/sync/v9${endpoint}`,
uri: `https://api.todoist.com/sync/v9${endpoint.startsWith('/') ? endpoint : '/' + endpoint}`,

const elementType =
baseType === 'string' || baseType === 'number' || baseType === 'boolean' ? baseType : 'string';

const validator = createElementValidator(elementType as 'string' | 'number' | 'boolean');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rule violated: Prefer Typeguards over Type casting

Avoid using as for type narrowing; implement a type guard to verify elementType is 'string' | 'number' | 'boolean' before passing it to createElementValidator.

Prompt for AI agents
Address the following comment on packages/nodes-base/utils/types.ts at line 92:

<comment>Avoid using `as` for type narrowing; implement a type guard to verify `elementType` is `&#39;string&#39; | &#39;number&#39; | &#39;boolean&#39;` before passing it to `createElementValidator`.</comment>

<file context>
@@ -28,67 +38,130 @@ export function assertIsArray&lt;T&gt;(
 	);
 }
 
+type ParameterType =
+	| &#39;string&#39;
+	| &#39;boolean&#39;
+	| &#39;number&#39;
+	| &#39;resource-locator&#39;
+	| &#39;string[]&#39;
</file context>

@@ -737,11 +1622,43 @@ export class TodoistV2 implements INodeType {
const service = new TodoistService();
let responseData;
const resource = this.getNodeParameter('resource', 0);
const operation = this.getNodeParameter('operation', 0) as OperationType;
const operation = this.getNodeParameter('operation', 0) as TaskOperationType;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rule violated: Prefer Typeguards over Type casting

Direct type casting with as TaskOperationType violates the “Prefer Typeguards over Type casting” rule. Use the generic overload of getNodeParameter instead of a type assertion.

Prompt for AI agents
Address the following comment on packages/nodes-base/nodes/Todoist/v2/TodoistV2.node.ts at line 1625:

<comment>Direct type casting with `as TaskOperationType` violates the “Prefer Typeguards over Type casting” rule. Use the generic overload of `getNodeParameter` instead of a type assertion.</comment>

<file context>
@@ -737,11 +1622,43 @@ export class TodoistV2 implements INodeType {
 		const service = new TodoistService();
 		let responseData;
 		const resource = this.getNodeParameter(&#39;resource&#39;, 0);
-		const operation = this.getNodeParameter(&#39;operation&#39;, 0) as OperationType;
+		const operation = this.getNodeParameter(&#39;operation&#39;, 0) as TaskOperationType;
 		for (let i = 0; i &lt; length; i++) {
 			try {
</file context>
Suggested change
const operation = this.getNodeParameter('operation', 0) as TaskOperationType;
const operation = this.getNodeParameter<TaskOperationType>('operation', 0);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
n8n team Authored by the n8n team node/improvement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants