first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
IHookFunctions,
|
||||
ILoadOptionsFunctions,
|
||||
IDataObject,
|
||||
JsonObject,
|
||||
IHttpRequestMethods,
|
||||
IRequestOptions,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeApiError } from 'n8n-workflow';
|
||||
|
||||
export type Context = IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions;
|
||||
|
||||
export function FormatDueDatetime(isoString: string): string {
|
||||
// Assuming that the problem with incorrect date format was caused by milliseconds
|
||||
// Replacing the last 5 characters of ISO-formatted string with just Z char
|
||||
return isoString.replace(new RegExp('.000Z$'), 'Z');
|
||||
}
|
||||
|
||||
export async function todoistApiRequest(
|
||||
this: Context,
|
||||
method: IHttpRequestMethods,
|
||||
resource: string,
|
||||
body: IDataObject = {},
|
||||
qs: IDataObject = {},
|
||||
): Promise<any> {
|
||||
const authentication = this.getNodeParameter('authentication', 0) as string;
|
||||
|
||||
const nodeVersion = this.getNode().typeVersion;
|
||||
const endpoint = nodeVersion >= 2.2 ? 'api.todoist.com/api/v1' : 'api.todoist.com/rest/v2';
|
||||
|
||||
const options: IRequestOptions = {
|
||||
method,
|
||||
qs,
|
||||
uri: `https://${endpoint}${resource}`,
|
||||
json: true,
|
||||
};
|
||||
|
||||
if (Object.keys(body).length !== 0) {
|
||||
options.body = body;
|
||||
}
|
||||
|
||||
try {
|
||||
const credentialType = authentication === 'apiKey' ? 'todoistApi' : 'todoistOAuth2Api';
|
||||
return await this.helpers.requestWithAuthentication.call(this, credentialType, options);
|
||||
} catch (error) {
|
||||
throw new NodeApiError(this.getNode(), error as JsonObject);
|
||||
}
|
||||
}
|
||||
|
||||
export async function todoistSyncRequest(
|
||||
this: Context,
|
||||
body: any = {},
|
||||
qs: IDataObject = {},
|
||||
endpoint: string = '/sync',
|
||||
): Promise<any> {
|
||||
const authentication = this.getNodeParameter('authentication', 0, 'oAuth2');
|
||||
|
||||
const options: IRequestOptions = {
|
||||
headers: {},
|
||||
method: 'POST',
|
||||
qs,
|
||||
uri: `https://api.todoist.com/sync/v9${endpoint}`,
|
||||
json: true,
|
||||
};
|
||||
|
||||
if (Object.keys(body as IDataObject).length !== 0) {
|
||||
options.body = body;
|
||||
}
|
||||
|
||||
try {
|
||||
const credentialType = authentication === 'oAuth2' ? 'todoistOAuth2Api' : 'todoistApi';
|
||||
return await this.helpers.requestWithAuthentication.call(this, credentialType, options);
|
||||
} catch (error) {
|
||||
throw new NodeApiError(this.getNode(), error as JsonObject);
|
||||
}
|
||||
}
|
||||
|
||||
export async function todoistApiGetAllRequest(
|
||||
ctx: Context,
|
||||
resource: string,
|
||||
qs: IDataObject = {},
|
||||
limit?: number,
|
||||
) {
|
||||
const nodeVersion = ctx.getNode().typeVersion;
|
||||
|
||||
if (nodeVersion < 2.2) {
|
||||
let response = await todoistApiRequest.call(ctx, 'GET', resource, {}, qs);
|
||||
if (limit) response = response.splice(0, limit);
|
||||
return response;
|
||||
}
|
||||
|
||||
if (limit !== undefined && limit <= 0) return [];
|
||||
|
||||
const results: IDataObject[] = [];
|
||||
let nextCursor: string | null = null;
|
||||
|
||||
do {
|
||||
const requestQs = { ...qs };
|
||||
if (nextCursor) requestQs.cursor = nextCursor;
|
||||
|
||||
if (limit !== undefined && limit > 0) {
|
||||
const remainingItems = limit - results.length;
|
||||
if (remainingItems <= 0) break;
|
||||
|
||||
requestQs.limit = Math.min(remainingItems, 200);
|
||||
}
|
||||
|
||||
const response = await todoistApiRequest.call(ctx, 'GET', resource, {}, requestQs);
|
||||
|
||||
if (response?.results && Array.isArray(response.results)) {
|
||||
if (limit !== undefined && limit > 0) {
|
||||
const remainingItems = limit - results.length;
|
||||
const itemsToAdd = response.results.slice(0, remainingItems);
|
||||
results.push(...itemsToAdd);
|
||||
|
||||
if (results.length >= limit) break;
|
||||
} else {
|
||||
results.push(...response.results);
|
||||
}
|
||||
|
||||
nextCursor = response.next_cursor ?? null;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} while (nextCursor !== null);
|
||||
|
||||
return results;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"node": "n8n-nodes-base.todoist",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"categories": ["Productivity"],
|
||||
"resources": {
|
||||
"credentialDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/credentials/todoist/"
|
||||
}
|
||||
],
|
||||
"primaryDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.todoist/"
|
||||
}
|
||||
],
|
||||
"generic": [
|
||||
{
|
||||
"label": "Benefits of automation and n8n: An interview with HubSpot's Hugh Durkin",
|
||||
"icon": "🎖",
|
||||
"url": "https://n8n.io/blog/benefits-of-automation-and-n8n-an-interview-with-hubspots-hugh-durkin/"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { INodeTypeBaseDescription, IVersionedNodeType } from 'n8n-workflow';
|
||||
import { VersionedNodeType } from 'n8n-workflow';
|
||||
|
||||
import { TodoistV1 } from './v1/TodoistV1.node';
|
||||
import { TodoistV2 } from './v2/TodoistV2.node';
|
||||
|
||||
export class Todoist extends VersionedNodeType {
|
||||
constructor() {
|
||||
const baseDescription: INodeTypeBaseDescription = {
|
||||
displayName: 'Todoist',
|
||||
name: 'todoist',
|
||||
icon: 'file:todoist.svg',
|
||||
group: ['output'],
|
||||
defaultVersion: 2.2,
|
||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||
description: 'Consume Todoist API',
|
||||
};
|
||||
|
||||
const nodeVersions: IVersionedNodeType['nodeVersions'] = {
|
||||
1: new TodoistV1(baseDescription),
|
||||
2: new TodoistV2(baseDescription),
|
||||
2.1: new TodoistV2(baseDescription),
|
||||
2.2: new TodoistV2(baseDescription),
|
||||
};
|
||||
|
||||
super(nodeVersions, baseDescription);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"assignee_id": {
|
||||
"type": "null"
|
||||
},
|
||||
"assigner_id": {
|
||||
"type": "null"
|
||||
},
|
||||
"comment_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"content": {
|
||||
"type": "string"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string"
|
||||
},
|
||||
"creator_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"deadline": {
|
||||
"type": "null"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"duration": {
|
||||
"type": "null"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"is_completed": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"labels": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"order": {
|
||||
"type": "integer"
|
||||
},
|
||||
"priority": {
|
||||
"type": "integer"
|
||||
},
|
||||
"project_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"comment_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"content": {
|
||||
"type": "string"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string"
|
||||
},
|
||||
"creator_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"due": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"date": {
|
||||
"type": "string"
|
||||
},
|
||||
"is_recurring": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"lang": {
|
||||
"type": "string"
|
||||
},
|
||||
"string": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"is_completed": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"labels": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"order": {
|
||||
"type": "integer"
|
||||
},
|
||||
"priority": {
|
||||
"type": "integer"
|
||||
},
|
||||
"project_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 2
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"comment_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"content": {
|
||||
"type": "string"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string"
|
||||
},
|
||||
"creator_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"is_completed": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"labels": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"order": {
|
||||
"type": "integer"
|
||||
},
|
||||
"priority": {
|
||||
"type": "integer"
|
||||
},
|
||||
"project_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,404 @@
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type { IExecuteFunctions, INode } from 'n8n-workflow';
|
||||
|
||||
import type { Context } from '../GenericFunctions';
|
||||
import { todoistApiGetAllRequest } from '../GenericFunctions';
|
||||
|
||||
const createMockContext = (typeVersion: number = 2.2) => {
|
||||
const mockRequestWithAuth = jest.fn();
|
||||
const mockCtx = mock<IExecuteFunctions>({
|
||||
getNode: () => mock<INode>({ typeVersion }),
|
||||
getNodeParameter: jest.fn((param: string) => {
|
||||
if (param === 'authentication') return 'oAuth2';
|
||||
return '';
|
||||
}) as any,
|
||||
helpers: {
|
||||
requestWithAuthentication: {
|
||||
call: mockRequestWithAuth,
|
||||
} as any,
|
||||
} as any,
|
||||
});
|
||||
|
||||
return {
|
||||
ctx: mockCtx as unknown as Context,
|
||||
mockRequestWithAuth,
|
||||
};
|
||||
};
|
||||
|
||||
describe('GenericFunctions', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('todoistApiGetAllRequest', () => {
|
||||
describe('Legacy mode (node version < 2.2)', () => {
|
||||
it('should fetch all items without pagination for version 2.0', async () => {
|
||||
const { ctx, mockRequestWithAuth } = createMockContext(2.0);
|
||||
const resource = '/tasks';
|
||||
const qs = { project_id: '123' };
|
||||
|
||||
const mockResponse = [
|
||||
{ id: '1', content: 'Task 1' },
|
||||
{ id: '2', content: 'Task 2' },
|
||||
{ id: '3', content: 'Task 3' },
|
||||
];
|
||||
|
||||
mockRequestWithAuth.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await todoistApiGetAllRequest(ctx, resource, qs);
|
||||
|
||||
expect(mockRequestWithAuth).toHaveBeenCalled();
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
it('should fetch all items without pagination for version 2.1', async () => {
|
||||
const { ctx, mockRequestWithAuth } = createMockContext(2.1);
|
||||
const resource = '/tasks';
|
||||
const qs = { project_id: '123' };
|
||||
|
||||
const mockResponse = [
|
||||
{ id: '1', content: 'Task 1' },
|
||||
{ id: '2', content: 'Task 2' },
|
||||
{ id: '3', content: 'Task 3' },
|
||||
];
|
||||
|
||||
mockRequestWithAuth.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await todoistApiGetAllRequest(ctx, resource, qs);
|
||||
|
||||
expect(mockRequestWithAuth).toHaveBeenCalled();
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
it('should apply limit when specified for old node versions', async () => {
|
||||
const { ctx, mockRequestWithAuth } = createMockContext(2.1);
|
||||
const resource = '/tasks';
|
||||
const qs = { project_id: '123' };
|
||||
const limit = 2;
|
||||
|
||||
const mockResponse = [
|
||||
{ id: '1', content: 'Task 1' },
|
||||
{ id: '2', content: 'Task 2' },
|
||||
{ id: '3', content: 'Task 3' },
|
||||
];
|
||||
|
||||
mockRequestWithAuth.mockResolvedValue([...mockResponse]);
|
||||
|
||||
const result = await todoistApiGetAllRequest(ctx, resource, qs, limit);
|
||||
|
||||
expect(mockRequestWithAuth).toHaveBeenCalled();
|
||||
expect(result).toEqual([
|
||||
{ id: '1', content: 'Task 1' },
|
||||
{ id: '2', content: 'Task 2' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle limit greater than available items', async () => {
|
||||
const { ctx, mockRequestWithAuth } = createMockContext(2.1);
|
||||
const resource = '/tasks';
|
||||
const limit = 10;
|
||||
|
||||
const mockResponse = [
|
||||
{ id: '1', content: 'Task 1' },
|
||||
{ id: '2', content: 'Task 2' },
|
||||
];
|
||||
|
||||
mockRequestWithAuth.mockResolvedValue([...mockResponse]);
|
||||
|
||||
const result = await todoistApiGetAllRequest(ctx, resource, {}, limit);
|
||||
|
||||
expect(mockRequestWithAuth).toHaveBeenCalled();
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
it('should handle empty response for old node versions', async () => {
|
||||
const { ctx, mockRequestWithAuth } = createMockContext(2.1);
|
||||
const resource = '/tasks';
|
||||
|
||||
mockRequestWithAuth.mockResolvedValue([]);
|
||||
|
||||
const result = await todoistApiGetAllRequest(ctx, resource);
|
||||
|
||||
expect(mockRequestWithAuth).toHaveBeenCalled();
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle query parameters correctly', async () => {
|
||||
const { ctx, mockRequestWithAuth } = createMockContext(2.0);
|
||||
const resource = '/tasks';
|
||||
const qs = { project_id: '456', filter: 'today' };
|
||||
|
||||
const mockResponse = [{ id: '1', content: 'Task 1' }];
|
||||
|
||||
mockRequestWithAuth.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await todoistApiGetAllRequest(ctx, resource, qs);
|
||||
|
||||
expect(mockRequestWithAuth).toHaveBeenCalled();
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
it('should work without query parameters', async () => {
|
||||
const { ctx, mockRequestWithAuth } = createMockContext(2.1);
|
||||
const resource = '/projects';
|
||||
|
||||
const mockResponse = [
|
||||
{ id: '1', name: 'Project 1' },
|
||||
{ id: '2', name: 'Project 2' },
|
||||
];
|
||||
|
||||
mockRequestWithAuth.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await todoistApiGetAllRequest(ctx, resource);
|
||||
|
||||
expect(mockRequestWithAuth).toHaveBeenCalled();
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Pagination mode (node version >= 2.2)', () => {
|
||||
it('should fetch all items with pagination for version 2.2', async () => {
|
||||
const { ctx, mockRequestWithAuth } = createMockContext(2.2);
|
||||
const resource = '/tasks';
|
||||
const qs = { project_id: '123' };
|
||||
|
||||
const mockResponsePage1 = {
|
||||
results: [
|
||||
{ id: '1', content: 'Task 1' },
|
||||
{ id: '2', content: 'Task 2' },
|
||||
],
|
||||
next_cursor: 'cursor-123',
|
||||
};
|
||||
|
||||
const mockResponsePage2 = {
|
||||
results: [
|
||||
{ id: '3', content: 'Task 3' },
|
||||
{ id: '4', content: 'Task 4' },
|
||||
],
|
||||
next_cursor: null,
|
||||
};
|
||||
|
||||
mockRequestWithAuth
|
||||
.mockResolvedValueOnce(mockResponsePage1)
|
||||
.mockResolvedValueOnce(mockResponsePage2);
|
||||
|
||||
const result = await todoistApiGetAllRequest(ctx, resource, qs);
|
||||
|
||||
expect(mockRequestWithAuth).toHaveBeenCalledTimes(2);
|
||||
expect(result).toEqual([
|
||||
{ id: '1', content: 'Task 1' },
|
||||
{ id: '2', content: 'Task 2' },
|
||||
{ id: '3', content: 'Task 3' },
|
||||
{ id: '4', content: 'Task 4' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should fetch all items with pagination for version greater then 2.2', async () => {
|
||||
const { ctx, mockRequestWithAuth } = createMockContext(2.3);
|
||||
const resource = '/projects';
|
||||
|
||||
const mockResponsePage1 = {
|
||||
results: [{ id: '1', name: 'Project 1' }],
|
||||
next_cursor: 'next-page',
|
||||
};
|
||||
|
||||
const mockResponsePage2 = {
|
||||
results: [{ id: '2', name: 'Project 2' }],
|
||||
next_cursor: null,
|
||||
};
|
||||
|
||||
mockRequestWithAuth
|
||||
.mockResolvedValueOnce(mockResponsePage1)
|
||||
.mockResolvedValueOnce(mockResponsePage2);
|
||||
|
||||
const result = await todoistApiGetAllRequest(ctx, resource);
|
||||
|
||||
expect(mockRequestWithAuth).toHaveBeenCalledTimes(2);
|
||||
expect(result).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should respect limit and stop fetching when limit is reached', async () => {
|
||||
const { ctx, mockRequestWithAuth } = createMockContext(2.2);
|
||||
const resource = '/tasks';
|
||||
const qs = { project_id: '123' };
|
||||
const limit = 3;
|
||||
|
||||
const mockResponsePage1 = {
|
||||
results: [
|
||||
{ id: '1', content: 'Task 1' },
|
||||
{ id: '2', content: 'Task 2' },
|
||||
],
|
||||
next_cursor: 'cursor-123',
|
||||
};
|
||||
|
||||
const mockResponsePage2 = {
|
||||
results: [
|
||||
{ id: '3', content: 'Task 3' },
|
||||
{ id: '4', content: 'Task 4' },
|
||||
],
|
||||
next_cursor: 'cursor-456',
|
||||
};
|
||||
|
||||
mockRequestWithAuth
|
||||
.mockResolvedValueOnce(mockResponsePage1)
|
||||
.mockResolvedValueOnce(mockResponsePage2);
|
||||
|
||||
const result = await todoistApiGetAllRequest(ctx, resource, qs, limit);
|
||||
|
||||
expect(mockRequestWithAuth).toHaveBeenCalledTimes(2);
|
||||
expect(result).toEqual([
|
||||
{ id: '1', content: 'Task 1' },
|
||||
{ id: '2', content: 'Task 2' },
|
||||
{ id: '3', content: 'Task 3' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle single page response with no next cursor', async () => {
|
||||
const { ctx, mockRequestWithAuth } = createMockContext(2.2);
|
||||
const resource = '/tasks';
|
||||
|
||||
const mockResponse = {
|
||||
results: [
|
||||
{ id: '1', content: 'Task 1' },
|
||||
{ id: '2', content: 'Task 2' },
|
||||
],
|
||||
next_cursor: null,
|
||||
};
|
||||
|
||||
mockRequestWithAuth.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await todoistApiGetAllRequest(ctx, resource);
|
||||
|
||||
expect(mockRequestWithAuth).toHaveBeenCalledTimes(1);
|
||||
expect(result).toEqual([
|
||||
{ id: '1', content: 'Task 1' },
|
||||
{ id: '2', content: 'Task 2' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle empty results array', async () => {
|
||||
const { ctx, mockRequestWithAuth } = createMockContext(2.2);
|
||||
const resource = '/tasks';
|
||||
|
||||
const mockResponse = {
|
||||
results: [],
|
||||
next_cursor: null,
|
||||
};
|
||||
|
||||
mockRequestWithAuth.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await todoistApiGetAllRequest(ctx, resource);
|
||||
|
||||
expect(mockRequestWithAuth).toHaveBeenCalledTimes(1);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle response without results property', async () => {
|
||||
const { ctx, mockRequestWithAuth } = createMockContext(2.2);
|
||||
const resource = '/tasks';
|
||||
|
||||
const mockResponse = {
|
||||
next_cursor: null,
|
||||
};
|
||||
|
||||
mockRequestWithAuth.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await todoistApiGetAllRequest(ctx, resource);
|
||||
|
||||
expect(mockRequestWithAuth).toHaveBeenCalledTimes(1);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should respect limit of 200 items per request', async () => {
|
||||
const { ctx, mockRequestWithAuth } = createMockContext(2.2);
|
||||
const resource = '/tasks';
|
||||
const limit = 250;
|
||||
|
||||
const mockResponsePage1 = {
|
||||
results: Array.from({ length: 200 }, (_, i) => ({ id: String(i + 1) })),
|
||||
next_cursor: 'cursor-123',
|
||||
};
|
||||
|
||||
const mockResponsePage2 = {
|
||||
results: Array.from({ length: 50 }, (_, i) => ({ id: String(i + 201) })),
|
||||
next_cursor: null,
|
||||
};
|
||||
|
||||
mockRequestWithAuth
|
||||
.mockResolvedValueOnce(mockResponsePage1)
|
||||
.mockResolvedValueOnce(mockResponsePage2);
|
||||
|
||||
const result = await todoistApiGetAllRequest(ctx, resource, {}, limit);
|
||||
|
||||
expect(mockRequestWithAuth).toHaveBeenCalledTimes(2);
|
||||
expect(result).toHaveLength(250);
|
||||
});
|
||||
|
||||
it('should handle multiple pages with different result sizes', async () => {
|
||||
const { ctx, mockRequestWithAuth } = createMockContext(2.2);
|
||||
const resource = '/tasks';
|
||||
|
||||
const mockResponsePage1 = {
|
||||
results: [{ id: '1' }, { id: '2' }],
|
||||
next_cursor: 'cursor-1',
|
||||
};
|
||||
|
||||
const mockResponsePage2 = {
|
||||
results: [{ id: '3' }],
|
||||
next_cursor: 'cursor-2',
|
||||
};
|
||||
|
||||
const mockResponsePage3 = {
|
||||
results: [{ id: '4' }, { id: '5' }, { id: '6' }],
|
||||
next_cursor: null,
|
||||
};
|
||||
|
||||
mockRequestWithAuth
|
||||
.mockResolvedValueOnce(mockResponsePage1)
|
||||
.mockResolvedValueOnce(mockResponsePage2)
|
||||
.mockResolvedValueOnce(mockResponsePage3);
|
||||
|
||||
const result = await todoistApiGetAllRequest(ctx, resource);
|
||||
|
||||
expect(mockRequestWithAuth).toHaveBeenCalledTimes(3);
|
||||
expect(result).toEqual([
|
||||
{ id: '1' },
|
||||
{ id: '2' },
|
||||
{ id: '3' },
|
||||
{ id: '4' },
|
||||
{ id: '5' },
|
||||
{ id: '6' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should stop pagination when limit is exactly reached', async () => {
|
||||
const { ctx, mockRequestWithAuth } = createMockContext(2.2);
|
||||
const resource = '/tasks';
|
||||
const limit = 2;
|
||||
|
||||
const mockResponsePage1 = {
|
||||
results: [{ id: '1' }, { id: '2' }],
|
||||
next_cursor: 'cursor-123',
|
||||
};
|
||||
|
||||
mockRequestWithAuth.mockResolvedValueOnce(mockResponsePage1);
|
||||
|
||||
const result = await todoistApiGetAllRequest(ctx, resource, {}, limit);
|
||||
|
||||
expect(mockRequestWithAuth).toHaveBeenCalledTimes(1);
|
||||
expect(result).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should handle limit of 0', async () => {
|
||||
const { ctx, mockRequestWithAuth } = createMockContext(2.2);
|
||||
const resource = '/tasks';
|
||||
const limit = 0;
|
||||
|
||||
const result = await todoistApiGetAllRequest(ctx, resource, {}, limit);
|
||||
|
||||
expect(mockRequestWithAuth).not.toHaveBeenCalled();
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60"><g fill="none"><path fill="#E44332" d="M52.5 0h-45A7.52 7.52 0 0 0 0 7.5v45C0 56.625 3.374 60 7.5 60h45c4.126 0 7.5-3.375 7.5-7.5v-45C60 3.375 56.626 0 52.5 0"/><path fill="#FFF" d="M12.121 29.582c1-.673 22.469-15.055 22.96-15.388.49-.329.518-1.341-.036-1.707-.55-.365-1.593-1.056-1.98-1.321a1.59 1.59 0 0 0-1.771.025A26391 26391 0 0 1 12.062 24.06c-.737.486-1.642.494-2.373 0L0 17.507v5.57c2.356 1.601 8.222 5.576 9.642 6.512.848.555 1.66.543 2.481-.007"/><path fill="#FFF" d="M12.121 39.128c1-.673 22.469-15.055 22.96-15.389.49-.329.518-1.34-.036-1.707a234 234 0 0 1-1.98-1.32 1.59 1.59 0 0 0-1.771.024c-.275.185-18.62 12.467-19.232 12.87-.737.486-1.642.494-2.373 0L0 27.052v5.57c2.356 1.602 8.222 5.576 9.642 6.512.848.556 1.66.543 2.481-.006"/><path fill="#FFF" d="M12.121 48.674c1-.673 22.469-15.056 22.96-15.39.49-.328.518-1.34-.036-1.707-.55-.363-1.593-1.055-1.98-1.32a1.59 1.59 0 0 0-1.771.024c-.275.185-18.62 12.467-19.232 12.87-.737.486-1.642.495-2.373 0L0 36.598v5.571c2.356 1.601 8.222 5.576 9.642 6.512.848.555 1.66.543 2.481-.006"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,357 @@
|
||||
import type { IDataObject } from 'n8n-workflow';
|
||||
import { ApplicationError, jsonParse } from 'n8n-workflow';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import type { Section, TodoistResponse } from './Service';
|
||||
import type { Context } from '../GenericFunctions';
|
||||
import { FormatDueDatetime, todoistApiRequest, todoistSyncRequest } from '../GenericFunctions';
|
||||
|
||||
export interface OperationHandler {
|
||||
handleOperation(ctx: Context, itemIndex: number): Promise<TodoistResponse>;
|
||||
}
|
||||
|
||||
export interface CreateTaskRequest {
|
||||
content?: string;
|
||||
description?: string;
|
||||
project_id?: number;
|
||||
section_id?: number;
|
||||
parent_id?: string;
|
||||
order?: number;
|
||||
labels?: string[];
|
||||
priority?: number;
|
||||
due_string?: string;
|
||||
due_datetime?: string;
|
||||
due_date?: string;
|
||||
due_lang?: string;
|
||||
}
|
||||
|
||||
export interface SyncRequest {
|
||||
commands: Command[];
|
||||
temp_id_mapping?: IDataObject;
|
||||
}
|
||||
|
||||
export interface Command {
|
||||
type: CommandType;
|
||||
uuid: string;
|
||||
temp_id?: string;
|
||||
args: {
|
||||
id?: number;
|
||||
section_id?: number;
|
||||
project_id?: number | string;
|
||||
section?: string;
|
||||
content?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const CommandTypes = {
|
||||
ITEM_MOVE: 'item_move',
|
||||
ITEM_ADD: 'item_add',
|
||||
ITEM_UPDATE: 'item_update',
|
||||
ITEM_REORDER: 'item_reorder',
|
||||
ITEM_DELETE: 'item_delete',
|
||||
ITEM_COMPLETE: 'item_complete',
|
||||
} as const;
|
||||
|
||||
export type CommandType = (typeof CommandTypes)[keyof typeof CommandTypes];
|
||||
|
||||
async function getLabelNameFromId(ctx: Context, labelIds: number[]): Promise<string[]> {
|
||||
const labelList = [];
|
||||
for (const label of labelIds) {
|
||||
const thisLabel = await todoistApiRequest.call(ctx, 'GET', `/labels/${label}`);
|
||||
labelList.push(thisLabel.name);
|
||||
}
|
||||
return labelList;
|
||||
}
|
||||
|
||||
export class CreateHandler implements OperationHandler {
|
||||
async handleOperation(ctx: Context, itemIndex: number): Promise<TodoistResponse> {
|
||||
//https://developer.todoist.com/rest/v2/#create-a-new-task
|
||||
const content = ctx.getNodeParameter('content', itemIndex) as string;
|
||||
const projectId = ctx.getNodeParameter('project', itemIndex, undefined, {
|
||||
extractValue: true,
|
||||
}) as number;
|
||||
const labels = ctx.getNodeParameter('labels', itemIndex) as number[];
|
||||
const options = ctx.getNodeParameter('options', itemIndex) as IDataObject;
|
||||
|
||||
const body: CreateTaskRequest = {
|
||||
content,
|
||||
project_id: projectId,
|
||||
priority: options.priority! ? parseInt(options.priority as string, 10) : 1,
|
||||
};
|
||||
|
||||
if (options.description) {
|
||||
body.description = options.description as string;
|
||||
}
|
||||
|
||||
if (options.dueDateTime) {
|
||||
body.due_datetime = FormatDueDatetime(options.dueDateTime as string);
|
||||
}
|
||||
|
||||
if (options.dueString) {
|
||||
body.due_string = options.dueString as string;
|
||||
}
|
||||
|
||||
if (labels !== undefined && labels.length !== 0) {
|
||||
body.labels = await getLabelNameFromId(ctx, labels);
|
||||
}
|
||||
|
||||
if (options.section) {
|
||||
body.section_id = options.section as number;
|
||||
}
|
||||
|
||||
if (options.dueLang) {
|
||||
body.due_lang = options.dueLang as string;
|
||||
}
|
||||
|
||||
if (options.parentId) {
|
||||
body.parent_id = options.parentId as string;
|
||||
}
|
||||
|
||||
const data = await todoistApiRequest.call(ctx, 'POST', '/tasks', body as IDataObject);
|
||||
|
||||
return {
|
||||
data,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class CloseHandler implements OperationHandler {
|
||||
async handleOperation(ctx: Context, itemIndex: number): Promise<TodoistResponse> {
|
||||
const id = ctx.getNodeParameter('taskId', itemIndex) as string;
|
||||
|
||||
await todoistApiRequest.call(ctx, 'POST', `/tasks/${id}/close`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class DeleteHandler implements OperationHandler {
|
||||
async handleOperation(ctx: Context, itemIndex: number): Promise<TodoistResponse> {
|
||||
const id = ctx.getNodeParameter('taskId', itemIndex) as string;
|
||||
|
||||
await todoistApiRequest.call(ctx, 'DELETE', `/tasks/${id}`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class GetHandler implements OperationHandler {
|
||||
async handleOperation(ctx: Context, itemIndex: number): Promise<TodoistResponse> {
|
||||
const id = ctx.getNodeParameter('taskId', itemIndex) as string;
|
||||
|
||||
const responseData = await todoistApiRequest.call(ctx, 'GET', `/tasks/${id}`);
|
||||
return {
|
||||
data: responseData,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class GetAllHandler implements OperationHandler {
|
||||
async handleOperation(ctx: Context, itemIndex: number): Promise<TodoistResponse> {
|
||||
//https://developer.todoist.com/rest/v2/#get-active-tasks
|
||||
const returnAll = ctx.getNodeParameter('returnAll', itemIndex) as boolean;
|
||||
const filters = ctx.getNodeParameter('filters', itemIndex) as IDataObject;
|
||||
const qs: IDataObject = {};
|
||||
|
||||
if (filters.projectId) {
|
||||
qs.project_id = filters.projectId as string;
|
||||
}
|
||||
if (filters.labelId) {
|
||||
qs.label = filters.labelId as string;
|
||||
}
|
||||
if (filters.filter) {
|
||||
qs.filter = filters.filter as string;
|
||||
}
|
||||
if (filters.lang) {
|
||||
qs.lang = filters.lang as string;
|
||||
}
|
||||
if (filters.ids) {
|
||||
qs.ids = filters.ids as string;
|
||||
}
|
||||
|
||||
let responseData = await todoistApiRequest.call(ctx, 'GET', '/tasks', {}, qs);
|
||||
|
||||
if (!returnAll) {
|
||||
const limit = ctx.getNodeParameter('limit', itemIndex) as number;
|
||||
responseData = responseData.splice(0, limit);
|
||||
}
|
||||
|
||||
return {
|
||||
data: responseData,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function getSectionIds(ctx: Context, projectId: number): Promise<Map<string, number>> {
|
||||
const sections: Section[] = await todoistApiRequest.call(
|
||||
ctx,
|
||||
'GET',
|
||||
'/sections',
|
||||
{},
|
||||
{ project_id: projectId },
|
||||
);
|
||||
return new Map(sections.map((s) => [s.name, s.id as unknown as number]));
|
||||
}
|
||||
|
||||
export class ReopenHandler implements OperationHandler {
|
||||
async handleOperation(ctx: Context, itemIndex: number): Promise<TodoistResponse> {
|
||||
//https://developer.todoist.com/rest/v2/#get-an-active-task
|
||||
const id = ctx.getNodeParameter('taskId', itemIndex) as string;
|
||||
|
||||
await todoistApiRequest.call(ctx, 'POST', `/tasks/${id}/reopen`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class UpdateHandler implements OperationHandler {
|
||||
async handleOperation(ctx: Context, itemIndex: number): Promise<TodoistResponse> {
|
||||
//https://developer.todoist.com/rest/v2/#update-a-task
|
||||
const id = ctx.getNodeParameter('taskId', itemIndex) as string;
|
||||
const updateFields = ctx.getNodeParameter('updateFields', itemIndex) as IDataObject;
|
||||
|
||||
const body: CreateTaskRequest = {};
|
||||
|
||||
if (updateFields.content) {
|
||||
body.content = updateFields.content as string;
|
||||
}
|
||||
|
||||
if (updateFields.priority) {
|
||||
body.priority = parseInt(updateFields.priority as string, 10);
|
||||
}
|
||||
|
||||
if (updateFields.description) {
|
||||
body.description = updateFields.description as string;
|
||||
}
|
||||
|
||||
if (updateFields.dueDateTime) {
|
||||
body.due_datetime = FormatDueDatetime(updateFields.dueDateTime as string);
|
||||
}
|
||||
|
||||
if (updateFields.dueString) {
|
||||
body.due_string = updateFields.dueString as string;
|
||||
}
|
||||
|
||||
if (
|
||||
updateFields.labels !== undefined &&
|
||||
Array.isArray(updateFields.labels) &&
|
||||
updateFields.labels.length !== 0
|
||||
) {
|
||||
body.labels = await getLabelNameFromId(ctx, updateFields.labels as number[]);
|
||||
}
|
||||
|
||||
if (updateFields.dueLang) {
|
||||
body.due_lang = updateFields.dueLang as string;
|
||||
}
|
||||
|
||||
await todoistApiRequest.call(ctx, 'POST', `/tasks/${id}`, body as IDataObject);
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
|
||||
export class MoveHandler implements OperationHandler {
|
||||
async handleOperation(ctx: Context, itemIndex: number): Promise<TodoistResponse> {
|
||||
//https://api.todoist.com/sync/v9/sync
|
||||
const taskId = ctx.getNodeParameter('taskId', itemIndex) as number;
|
||||
const section = ctx.getNodeParameter('section', itemIndex) as number;
|
||||
|
||||
const body: SyncRequest = {
|
||||
commands: [
|
||||
{
|
||||
type: CommandTypes.ITEM_MOVE,
|
||||
uuid: uuid(),
|
||||
args: {
|
||||
id: taskId,
|
||||
section_id: section,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await todoistSyncRequest.call(ctx, body);
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
|
||||
export class SyncHandler implements OperationHandler {
|
||||
async handleOperation(ctx: Context, itemIndex: number): Promise<TodoistResponse> {
|
||||
const commandsJson = ctx.getNodeParameter('commands', itemIndex) as string;
|
||||
const projectId = ctx.getNodeParameter('project', itemIndex, undefined, {
|
||||
extractValue: true,
|
||||
}) as number;
|
||||
const sections = await getSectionIds(ctx, projectId);
|
||||
const commands: Command[] = jsonParse(commandsJson);
|
||||
const tempIdMapping = new Map<string, string>();
|
||||
|
||||
for (let i = 0; i < commands.length; i++) {
|
||||
const command = commands[i];
|
||||
this.enrichUUID(command);
|
||||
this.enrichSection(command, sections);
|
||||
this.enrichProjectId(command, projectId);
|
||||
this.enrichTempId(command, tempIdMapping, projectId);
|
||||
}
|
||||
|
||||
const body: SyncRequest = {
|
||||
commands,
|
||||
temp_id_mapping: this.convertToObject(tempIdMapping),
|
||||
};
|
||||
|
||||
await todoistSyncRequest.call(ctx, body);
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
private convertToObject(map: Map<string, string>) {
|
||||
return Array.from(map.entries()).reduce((o, [key, value]) => {
|
||||
o[key] = value;
|
||||
return o;
|
||||
}, {} as IDataObject);
|
||||
}
|
||||
|
||||
private enrichUUID(command: Command) {
|
||||
command.uuid = uuid();
|
||||
}
|
||||
|
||||
private enrichSection(command: Command, sections: Map<string, number>) {
|
||||
if (command.args?.section !== undefined) {
|
||||
const sectionId = sections.get(command.args.section);
|
||||
if (sectionId) {
|
||||
command.args.section_id = sectionId;
|
||||
} else {
|
||||
throw new ApplicationError(
|
||||
'Section ' + command.args.section + " doesn't exist on Todoist",
|
||||
{ level: 'warning' },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enrichProjectId(command: Command, projectId: number) {
|
||||
if (this.requiresProjectId(command)) {
|
||||
command.args.project_id = projectId;
|
||||
}
|
||||
}
|
||||
|
||||
private requiresProjectId(command: Command) {
|
||||
return command.type === CommandTypes.ITEM_ADD;
|
||||
}
|
||||
|
||||
private enrichTempId(command: Command, tempIdMapping: Map<string, string>, projectId: number) {
|
||||
if (this.requiresTempId(command)) {
|
||||
command.temp_id = uuid();
|
||||
tempIdMapping.set(command.temp_id, projectId as unknown as string);
|
||||
}
|
||||
}
|
||||
|
||||
private requiresTempId(command: Command) {
|
||||
return command.type === CommandTypes.ITEM_ADD;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { IDataObject } from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
CloseHandler,
|
||||
CreateHandler,
|
||||
DeleteHandler,
|
||||
GetAllHandler,
|
||||
GetHandler,
|
||||
MoveHandler,
|
||||
ReopenHandler,
|
||||
SyncHandler,
|
||||
UpdateHandler,
|
||||
} from './OperationHandler';
|
||||
import type { Context } from '../GenericFunctions';
|
||||
|
||||
export class TodoistService implements Service {
|
||||
async execute(
|
||||
ctx: Context,
|
||||
operation: OperationType,
|
||||
itemIndex: number,
|
||||
): Promise<TodoistResponse> {
|
||||
return await this.handlers[operation].handleOperation(ctx, itemIndex);
|
||||
}
|
||||
|
||||
private handlers = {
|
||||
create: new CreateHandler(),
|
||||
close: new CloseHandler(),
|
||||
delete: new DeleteHandler(),
|
||||
get: new GetHandler(),
|
||||
getAll: new GetAllHandler(),
|
||||
reopen: new ReopenHandler(),
|
||||
update: new UpdateHandler(),
|
||||
move: new MoveHandler(),
|
||||
sync: new SyncHandler(),
|
||||
};
|
||||
}
|
||||
|
||||
export type OperationType =
|
||||
| 'create'
|
||||
| 'close'
|
||||
| 'delete'
|
||||
| 'get'
|
||||
| 'getAll'
|
||||
| 'reopen'
|
||||
| 'update'
|
||||
| 'move'
|
||||
| 'sync';
|
||||
|
||||
export interface Section {
|
||||
name: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface Service {
|
||||
execute(ctx: Context, operation: OperationType, itemIndex: number): Promise<TodoistResponse>;
|
||||
}
|
||||
|
||||
export interface TodoistResponse {
|
||||
success?: boolean;
|
||||
data?: IDataObject;
|
||||
}
|
||||
@@ -0,0 +1,730 @@
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
IDataObject,
|
||||
ILoadOptionsFunctions,
|
||||
INodeExecutionData,
|
||||
INodeListSearchResult,
|
||||
INodePropertyOptions,
|
||||
INodeType,
|
||||
INodeTypeBaseDescription,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeConnectionTypes } from 'n8n-workflow';
|
||||
|
||||
import type { OperationType } from './Service';
|
||||
import { TodoistService } from './Service';
|
||||
import { todoistApiRequest } from '../GenericFunctions';
|
||||
|
||||
// interface IBodyCreateTask {
|
||||
// content?: string;
|
||||
// description?: string;
|
||||
// project_id?: number;
|
||||
// section_id?: number;
|
||||
// parent_id?: number;
|
||||
// order?: number;
|
||||
// label_ids?: number[];
|
||||
// priority?: number;
|
||||
// due_string?: string;
|
||||
// due_datetime?: string;
|
||||
// due_date?: string;
|
||||
// due_lang?: string;
|
||||
// }
|
||||
|
||||
const versionDescription: INodeTypeDescription = {
|
||||
displayName: 'Todoist',
|
||||
name: 'todoist',
|
||||
icon: 'file:todoist.svg',
|
||||
group: ['output'],
|
||||
version: 1,
|
||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||
description: 'Consume Todoist API',
|
||||
defaults: {
|
||||
name: 'Todoist',
|
||||
},
|
||||
inputs: [NodeConnectionTypes.Main],
|
||||
outputs: [NodeConnectionTypes.Main],
|
||||
credentials: [
|
||||
{
|
||||
name: 'todoistApi',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: ['apiKey'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'todoistOAuth2Api',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: ['oAuth2'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Authentication',
|
||||
name: 'authentication',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'API Key',
|
||||
value: 'apiKey',
|
||||
},
|
||||
{
|
||||
name: 'OAuth2',
|
||||
value: 'oAuth2',
|
||||
},
|
||||
],
|
||||
default: 'apiKey',
|
||||
},
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Task',
|
||||
value: 'task',
|
||||
description: 'Task resource',
|
||||
},
|
||||
],
|
||||
default: 'task',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['task'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Close',
|
||||
value: 'close',
|
||||
description: 'Close a task',
|
||||
action: 'Close a task',
|
||||
},
|
||||
{
|
||||
name: 'Create',
|
||||
value: 'create',
|
||||
description: 'Create a new task',
|
||||
action: 'Create a task',
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
value: 'delete',
|
||||
description: 'Delete a task',
|
||||
action: 'Delete a task',
|
||||
},
|
||||
{
|
||||
name: 'Get',
|
||||
value: 'get',
|
||||
description: 'Get a task',
|
||||
action: 'Get a task',
|
||||
},
|
||||
{
|
||||
name: 'Get Many',
|
||||
value: 'getAll',
|
||||
description: 'Get many tasks',
|
||||
action: 'Get many tasks',
|
||||
},
|
||||
{
|
||||
name: 'Move',
|
||||
value: 'move',
|
||||
description: 'Move a task',
|
||||
action: 'Move a task',
|
||||
},
|
||||
{
|
||||
name: 'Reopen',
|
||||
value: 'reopen',
|
||||
description: 'Reopen a task',
|
||||
action: 'Reopen a task',
|
||||
},
|
||||
// {
|
||||
// name: 'Sync',
|
||||
// value: 'sync',
|
||||
// description: 'Sync a project',
|
||||
// },
|
||||
{
|
||||
name: 'Update',
|
||||
value: 'update',
|
||||
description: 'Update a task',
|
||||
action: 'Update a task',
|
||||
},
|
||||
],
|
||||
default: 'create',
|
||||
},
|
||||
{
|
||||
displayName: 'Task ID',
|
||||
name: 'taskId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['task'],
|
||||
operation: ['delete', 'close', 'get', 'reopen', 'update', 'move'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Project Name or ID',
|
||||
name: 'project',
|
||||
type: 'resourceLocator',
|
||||
default: { mode: 'list', value: '' },
|
||||
required: true,
|
||||
modes: [
|
||||
{
|
||||
displayName: 'From List',
|
||||
name: 'list',
|
||||
type: 'list',
|
||||
placeholder: 'Select a project...',
|
||||
typeOptions: {
|
||||
searchListMethod: 'searchProjects',
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
placeholder: '2302163813',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['task'],
|
||||
operation: ['create', 'move', 'sync'],
|
||||
},
|
||||
},
|
||||
description: 'The project you want to operate on. Choose from the list, or specify an ID.',
|
||||
},
|
||||
{
|
||||
displayName: 'Section Name or ID',
|
||||
name: 'section',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getSections',
|
||||
loadOptionsDependsOn: ['project'],
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['task'],
|
||||
operation: ['move'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'Section to which you want move the task. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
},
|
||||
{
|
||||
displayName: 'Label Names or IDs',
|
||||
name: 'labels',
|
||||
type: 'multiOptions',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getLabels',
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['task'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
default: [],
|
||||
description:
|
||||
'Optional labels that will be assigned to a created task. Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
},
|
||||
{
|
||||
displayName: 'Content',
|
||||
name: 'content',
|
||||
type: 'string',
|
||||
typeOptions: {
|
||||
rows: 5,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['task'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
required: true,
|
||||
description: 'Task content',
|
||||
},
|
||||
{
|
||||
displayName: 'Sync Commands',
|
||||
name: 'commands',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['task'],
|
||||
operation: ['sync'],
|
||||
},
|
||||
},
|
||||
default: '[]',
|
||||
hint: 'See docs for possible commands: https://developer.todoist.com/sync/v8/#sync',
|
||||
description: 'Sync body',
|
||||
},
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add option',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['task'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Description',
|
||||
name: 'description',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'A description for the task',
|
||||
},
|
||||
{
|
||||
displayName: 'Due Date Time',
|
||||
name: 'dueDateTime',
|
||||
type: 'dateTime',
|
||||
default: '',
|
||||
description: 'Specific date and time in RFC3339 format in UTC',
|
||||
},
|
||||
{
|
||||
displayName: 'Due String Locale',
|
||||
name: 'dueLang',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description:
|
||||
'2-letter code specifying language in case due_string is not written in English',
|
||||
},
|
||||
{
|
||||
displayName: 'Due String',
|
||||
name: 'dueString',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description:
|
||||
'Human defined task due date (ex.: “next Monday”, “Tomorrow”). Value is set using local (not UTC) time.',
|
||||
},
|
||||
{
|
||||
displayName: 'Parent Name or ID',
|
||||
name: 'parentId',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getItems',
|
||||
loadOptionsDependsOn: ['project', 'options.section'],
|
||||
},
|
||||
default: {},
|
||||
description:
|
||||
'The parent task you want to operate on. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
},
|
||||
{
|
||||
displayName: 'Priority',
|
||||
name: 'priority',
|
||||
type: 'number',
|
||||
typeOptions: {
|
||||
maxValue: 4,
|
||||
minValue: 1,
|
||||
},
|
||||
default: 1,
|
||||
description: 'Task priority from 1 (normal) to 4 (urgent)',
|
||||
},
|
||||
{
|
||||
displayName: 'Section Name or ID',
|
||||
name: 'section',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getSections',
|
||||
loadOptionsDependsOn: ['project'],
|
||||
},
|
||||
default: {},
|
||||
description:
|
||||
'The section you want to operate on. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Return All',
|
||||
name: 'returnAll',
|
||||
type: 'boolean',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['getAll'],
|
||||
resource: ['task'],
|
||||
},
|
||||
},
|
||||
default: false,
|
||||
description: 'Whether to return all results or only up to a given limit',
|
||||
},
|
||||
{
|
||||
displayName: 'Limit',
|
||||
name: 'limit',
|
||||
type: 'number',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['getAll'],
|
||||
resource: ['task'],
|
||||
returnAll: [false],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
maxValue: 500,
|
||||
},
|
||||
default: 50,
|
||||
description: 'Max number of results to return',
|
||||
},
|
||||
{
|
||||
displayName: 'Filters',
|
||||
name: 'filters',
|
||||
type: 'collection',
|
||||
placeholder: 'Add option',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['task'],
|
||||
operation: ['getAll'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Filter',
|
||||
name: 'filter',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description:
|
||||
'Filter by any <a href="https://get.todoist.help/hc/en-us/articles/205248842">supported filter.</a>',
|
||||
},
|
||||
{
|
||||
displayName: 'IDs',
|
||||
name: 'ids',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'A list of the task IDs to retrieve, this should be a comma-separated list',
|
||||
},
|
||||
{
|
||||
displayName: 'Label Name or ID',
|
||||
name: 'labelId',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getLabels',
|
||||
},
|
||||
default: {},
|
||||
description:
|
||||
'Filter tasks by label. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
},
|
||||
{
|
||||
displayName: 'Lang',
|
||||
name: 'lang',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description:
|
||||
'IETF language tag defining what language filter is written in, if differs from default English',
|
||||
},
|
||||
{
|
||||
displayName: 'Parent Name or ID',
|
||||
name: 'parentId',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getItems',
|
||||
loadOptionsDependsOn: ['filters.projectId', 'filters.sectionId'],
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'Filter tasks by parent task ID. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
},
|
||||
{
|
||||
displayName: 'Project Name or ID',
|
||||
name: 'projectId',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getProjects',
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'Filter tasks by project ID. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
},
|
||||
{
|
||||
displayName: 'Section Name or ID',
|
||||
name: 'sectionId',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getSections',
|
||||
loadOptionsDependsOn: ['filters.projectId'],
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'Filter tasks by section ID. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Update Fields',
|
||||
name: 'updateFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['task'],
|
||||
operation: ['update'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Content',
|
||||
name: 'content',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Task content',
|
||||
},
|
||||
{
|
||||
displayName: 'Description',
|
||||
name: 'description',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'A description for the task',
|
||||
},
|
||||
{
|
||||
displayName: 'Due Date Time',
|
||||
name: 'dueDateTime',
|
||||
type: 'dateTime',
|
||||
default: '',
|
||||
description: 'Specific date and time in RFC3339 format in UTC',
|
||||
},
|
||||
{
|
||||
displayName: 'Due String Locale',
|
||||
name: 'dueLang',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description:
|
||||
'2-letter code specifying language in case due_string is not written in English',
|
||||
},
|
||||
{
|
||||
displayName: 'Due String',
|
||||
name: 'dueString',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description:
|
||||
'Human defined task due date (ex.: “next Monday”, “Tomorrow”). Value is set using local (not UTC) time.',
|
||||
},
|
||||
{
|
||||
displayName: 'Due String Locale',
|
||||
name: 'dueLang',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description:
|
||||
'2-letter code specifying language in case due_string is not written in English',
|
||||
},
|
||||
{
|
||||
displayName: 'Label Names or IDs',
|
||||
name: 'labels',
|
||||
type: 'multiOptions',
|
||||
description:
|
||||
'Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getLabels',
|
||||
},
|
||||
default: [],
|
||||
},
|
||||
{
|
||||
displayName: 'Priority',
|
||||
name: 'priority',
|
||||
type: 'number',
|
||||
typeOptions: {
|
||||
maxValue: 4,
|
||||
minValue: 1,
|
||||
},
|
||||
default: 1,
|
||||
description: 'Task priority from 1 (normal) to 4 (urgent)',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export class TodoistV1 implements INodeType {
|
||||
description: INodeTypeDescription;
|
||||
|
||||
constructor(baseDescription: INodeTypeBaseDescription) {
|
||||
this.description = {
|
||||
...baseDescription,
|
||||
...versionDescription,
|
||||
};
|
||||
}
|
||||
|
||||
methods = {
|
||||
listSearch: {
|
||||
async searchProjects(this: ILoadOptionsFunctions): Promise<INodeListSearchResult> {
|
||||
const projects = await todoistApiRequest.call(this, 'GET', '/projects');
|
||||
return {
|
||||
results: projects.map((project: IDataObject) => ({
|
||||
name: project.name,
|
||||
value: project.id,
|
||||
})),
|
||||
};
|
||||
},
|
||||
async searchLabels(this: ILoadOptionsFunctions): Promise<INodeListSearchResult> {
|
||||
const labels = await todoistApiRequest.call(this, 'GET', '/labels');
|
||||
return {
|
||||
results: labels.map((label: IDataObject) => ({
|
||||
name: label.name,
|
||||
value: label.id,
|
||||
})),
|
||||
};
|
||||
},
|
||||
},
|
||||
loadOptions: {
|
||||
// Get all the available projects to display them to user so that they can
|
||||
// select them easily
|
||||
async getProjects(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const projects = await todoistApiRequest.call(this, 'GET', '/projects');
|
||||
for (const project of projects) {
|
||||
const projectName = project.name;
|
||||
const projectId = project.id;
|
||||
|
||||
returnData.push({
|
||||
name: projectName,
|
||||
value: projectId,
|
||||
});
|
||||
}
|
||||
|
||||
return returnData;
|
||||
},
|
||||
|
||||
// Get all the available sections in the selected project, to display them
|
||||
// to user so that they can select one easily
|
||||
async getSections(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
|
||||
const options = Object.assign(
|
||||
{},
|
||||
this.getNodeParameter('options', {}),
|
||||
this.getNodeParameter('filters', {}),
|
||||
) as IDataObject;
|
||||
|
||||
const projectId =
|
||||
(options.projectId as number) ??
|
||||
(this.getCurrentNodeParameter('project', { extractValue: true }) as number);
|
||||
if (projectId) {
|
||||
const qs: IDataObject = { project_id: projectId };
|
||||
const sections = await todoistApiRequest.call(this, 'GET', '/sections', {}, qs);
|
||||
for (const section of sections) {
|
||||
const sectionName = section.name;
|
||||
const sectionId = section.id;
|
||||
|
||||
returnData.push({
|
||||
name: sectionName,
|
||||
value: sectionId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return returnData;
|
||||
},
|
||||
|
||||
// Get all the available parents in the selected project and section,
|
||||
// to display them to user so that they can select one easily
|
||||
async getItems(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
|
||||
const options = Object.assign(
|
||||
{},
|
||||
this.getNodeParameter('options', {}),
|
||||
this.getNodeParameter('filters', {}),
|
||||
) as IDataObject;
|
||||
|
||||
const projectId =
|
||||
(options.projectId as number) ??
|
||||
(this.getCurrentNodeParameter('project', { extractValue: true }) as number);
|
||||
|
||||
const sectionId =
|
||||
(options.sectionId as number) ||
|
||||
(options.section as number) ||
|
||||
(this.getCurrentNodeParameter('sectionId') as number);
|
||||
|
||||
if (projectId) {
|
||||
const qs: IDataObject = sectionId
|
||||
? { project_id: projectId, section_id: sectionId }
|
||||
: { project_id: projectId };
|
||||
|
||||
const items = await todoistApiRequest.call(this, 'GET', '/tasks', {}, qs);
|
||||
for (const item of items) {
|
||||
const itemContent = item.content;
|
||||
const itemId = item.id;
|
||||
|
||||
returnData.push({
|
||||
name: itemContent,
|
||||
value: itemId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return returnData;
|
||||
},
|
||||
|
||||
// Get all the available labels to display them to user so that they can
|
||||
// select them easily
|
||||
async getLabels(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const labels = await todoistApiRequest.call(this, 'GET', '/labels');
|
||||
|
||||
for (const label of labels) {
|
||||
const labelName = label.name;
|
||||
const labelId = label.id;
|
||||
returnData.push({
|
||||
name: labelName,
|
||||
value: labelId,
|
||||
});
|
||||
}
|
||||
|
||||
return returnData;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
const returnData: IDataObject[] = [];
|
||||
const length = items.length;
|
||||
const service = new TodoistService();
|
||||
let responseData;
|
||||
const resource = this.getNodeParameter('resource', 0);
|
||||
const operation = this.getNodeParameter('operation', 0) as OperationType;
|
||||
for (let i = 0; i < length; i++) {
|
||||
try {
|
||||
if (resource === 'task') {
|
||||
responseData = await service.execute(this, operation, i);
|
||||
}
|
||||
if (Array.isArray(responseData?.data)) {
|
||||
returnData.push.apply(returnData, responseData?.data as IDataObject[]);
|
||||
} else {
|
||||
if (responseData?.hasOwnProperty('success')) {
|
||||
returnData.push({ success: responseData.success });
|
||||
} else {
|
||||
returnData.push(responseData?.data as IDataObject);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
returnData.push({ error: error.message });
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return [this.helpers.returnJsonArray(returnData)];
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,263 @@
|
||||
import type { IDataObject } from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
CloseHandler,
|
||||
CreateHandler,
|
||||
DeleteHandler,
|
||||
GetAllHandler,
|
||||
GetHandler,
|
||||
MoveHandler,
|
||||
QuickAddHandler,
|
||||
ReopenHandler,
|
||||
UpdateHandler,
|
||||
// Project handlers
|
||||
ProjectCreateHandler,
|
||||
ProjectDeleteHandler,
|
||||
ProjectGetHandler,
|
||||
ProjectGetAllHandler,
|
||||
ProjectUpdateHandler,
|
||||
ProjectArchiveHandler,
|
||||
ProjectUnarchiveHandler,
|
||||
ProjectGetCollaboratorsHandler,
|
||||
// Section handlers
|
||||
SectionCreateHandler,
|
||||
SectionDeleteHandler,
|
||||
SectionGetHandler,
|
||||
SectionGetAllHandler,
|
||||
SectionUpdateHandler,
|
||||
// Comment handlers
|
||||
CommentCreateHandler,
|
||||
CommentDeleteHandler,
|
||||
CommentGetHandler,
|
||||
CommentGetAllHandler,
|
||||
CommentUpdateHandler,
|
||||
// Label handlers
|
||||
LabelCreateHandler,
|
||||
LabelDeleteHandler,
|
||||
LabelGetHandler,
|
||||
LabelGetAllHandler,
|
||||
LabelUpdateHandler,
|
||||
// Reminder handlers
|
||||
ReminderCreateHandler,
|
||||
ReminderDeleteHandler,
|
||||
ReminderGetAllHandler,
|
||||
ReminderUpdateHandler,
|
||||
} from './OperationHandler';
|
||||
import type { Context } from '../GenericFunctions';
|
||||
|
||||
export class TodoistService implements Service {
|
||||
async executeTask(
|
||||
ctx: Context,
|
||||
operation: TaskOperationType,
|
||||
itemIndex: number,
|
||||
): Promise<TodoistResponse> {
|
||||
return await this.handlers[operation].handleOperation(ctx, itemIndex);
|
||||
}
|
||||
|
||||
private handlers = {
|
||||
create: new CreateHandler(),
|
||||
close: new CloseHandler(),
|
||||
delete: new DeleteHandler(),
|
||||
get: new GetHandler(),
|
||||
getAll: new GetAllHandler(),
|
||||
reopen: new ReopenHandler(),
|
||||
update: new UpdateHandler(),
|
||||
move: new MoveHandler(),
|
||||
quickAdd: new QuickAddHandler(),
|
||||
};
|
||||
|
||||
private projectHandlers = {
|
||||
create: new ProjectCreateHandler(),
|
||||
delete: new ProjectDeleteHandler(),
|
||||
get: new ProjectGetHandler(),
|
||||
getAll: new ProjectGetAllHandler(),
|
||||
update: new ProjectUpdateHandler(),
|
||||
archive: new ProjectArchiveHandler(),
|
||||
unarchive: new ProjectUnarchiveHandler(),
|
||||
getCollaborators: new ProjectGetCollaboratorsHandler(),
|
||||
};
|
||||
|
||||
private sectionHandlers = {
|
||||
create: new SectionCreateHandler(),
|
||||
delete: new SectionDeleteHandler(),
|
||||
get: new SectionGetHandler(),
|
||||
getAll: new SectionGetAllHandler(),
|
||||
update: new SectionUpdateHandler(),
|
||||
};
|
||||
|
||||
private commentHandlers = {
|
||||
create: new CommentCreateHandler(),
|
||||
delete: new CommentDeleteHandler(),
|
||||
get: new CommentGetHandler(),
|
||||
getAll: new CommentGetAllHandler(),
|
||||
update: new CommentUpdateHandler(),
|
||||
};
|
||||
|
||||
private labelHandlers = {
|
||||
create: new LabelCreateHandler(),
|
||||
delete: new LabelDeleteHandler(),
|
||||
get: new LabelGetHandler(),
|
||||
getAll: new LabelGetAllHandler(),
|
||||
update: new LabelUpdateHandler(),
|
||||
};
|
||||
|
||||
private reminderHandlers = {
|
||||
create: new ReminderCreateHandler(),
|
||||
delete: new ReminderDeleteHandler(),
|
||||
getAll: new ReminderGetAllHandler(),
|
||||
update: new ReminderUpdateHandler(),
|
||||
};
|
||||
|
||||
async executeProject(
|
||||
ctx: Context,
|
||||
operation: ProjectOperationType,
|
||||
itemIndex: number,
|
||||
): Promise<TodoistResponse> {
|
||||
return await this.projectHandlers[operation].handleOperation(ctx, itemIndex);
|
||||
}
|
||||
|
||||
async executeSection(
|
||||
ctx: Context,
|
||||
operation: SectionOperationType,
|
||||
itemIndex: number,
|
||||
): Promise<TodoistResponse> {
|
||||
return await this.sectionHandlers[operation].handleOperation(ctx, itemIndex);
|
||||
}
|
||||
|
||||
async executeComment(
|
||||
ctx: Context,
|
||||
operation: CommentOperationType,
|
||||
itemIndex: number,
|
||||
): Promise<TodoistResponse> {
|
||||
return await this.commentHandlers[operation].handleOperation(ctx, itemIndex);
|
||||
}
|
||||
|
||||
async executeLabel(
|
||||
ctx: Context,
|
||||
operation: LabelOperationType,
|
||||
itemIndex: number,
|
||||
): Promise<TodoistResponse> {
|
||||
return await this.labelHandlers[operation].handleOperation(ctx, itemIndex);
|
||||
}
|
||||
|
||||
async executeReminder(
|
||||
ctx: Context,
|
||||
operation: ReminderOperationType,
|
||||
itemIndex: number,
|
||||
): Promise<TodoistResponse> {
|
||||
return await this.reminderHandlers[operation].handleOperation(ctx, itemIndex);
|
||||
}
|
||||
}
|
||||
|
||||
// Define operations as const arrays - source of truth
|
||||
const TASK_OPERATIONS = [
|
||||
'create',
|
||||
'close',
|
||||
'delete',
|
||||
'get',
|
||||
'getAll',
|
||||
'reopen',
|
||||
'update',
|
||||
'move',
|
||||
'quickAdd',
|
||||
] as const;
|
||||
|
||||
const PROJECT_OPERATIONS = [
|
||||
'create',
|
||||
'delete',
|
||||
'get',
|
||||
'getAll',
|
||||
'update',
|
||||
'archive',
|
||||
'unarchive',
|
||||
'getCollaborators',
|
||||
] as const;
|
||||
|
||||
const SECTION_OPERATIONS = ['create', 'delete', 'get', 'getAll', 'update'] as const;
|
||||
|
||||
const COMMENT_OPERATIONS = ['create', 'delete', 'get', 'getAll', 'update'] as const;
|
||||
|
||||
const LABEL_OPERATIONS = ['create', 'delete', 'get', 'getAll', 'update'] as const;
|
||||
|
||||
const REMINDER_OPERATIONS = ['create', 'delete', 'getAll', 'update'] as const;
|
||||
|
||||
// Derive types from arrays
|
||||
export type TaskOperationType = (typeof TASK_OPERATIONS)[number];
|
||||
export type ProjectOperationType = (typeof PROJECT_OPERATIONS)[number];
|
||||
export type SectionOperationType = (typeof SECTION_OPERATIONS)[number];
|
||||
export type CommentOperationType = (typeof COMMENT_OPERATIONS)[number];
|
||||
export type LabelOperationType = (typeof LABEL_OPERATIONS)[number];
|
||||
export type ReminderOperationType = (typeof REMINDER_OPERATIONS)[number];
|
||||
|
||||
// Type guards using the same arrays
|
||||
export function isTaskOperationType(operation: string): operation is TaskOperationType {
|
||||
return TASK_OPERATIONS.includes(operation as TaskOperationType);
|
||||
}
|
||||
|
||||
export function isProjectOperationType(operation: string): operation is ProjectOperationType {
|
||||
return PROJECT_OPERATIONS.includes(operation as ProjectOperationType);
|
||||
}
|
||||
|
||||
export function isSectionOperationType(operation: string): operation is SectionOperationType {
|
||||
return SECTION_OPERATIONS.includes(operation as SectionOperationType);
|
||||
}
|
||||
|
||||
export function isCommentOperationType(operation: string): operation is CommentOperationType {
|
||||
return COMMENT_OPERATIONS.includes(operation as CommentOperationType);
|
||||
}
|
||||
|
||||
export function isLabelOperationType(operation: string): operation is LabelOperationType {
|
||||
return LABEL_OPERATIONS.includes(operation as LabelOperationType);
|
||||
}
|
||||
|
||||
export function isReminderOperationType(operation: string): operation is ReminderOperationType {
|
||||
return REMINDER_OPERATIONS.includes(operation as ReminderOperationType);
|
||||
}
|
||||
|
||||
export interface Section {
|
||||
name: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface Service {
|
||||
executeTask(
|
||||
ctx: Context,
|
||||
operation: TaskOperationType,
|
||||
itemIndex: number,
|
||||
): Promise<TodoistResponse>;
|
||||
executeProject(
|
||||
ctx: Context,
|
||||
operation: ProjectOperationType,
|
||||
itemIndex: number,
|
||||
): Promise<TodoistResponse>;
|
||||
executeSection(
|
||||
ctx: Context,
|
||||
operation: SectionOperationType,
|
||||
itemIndex: number,
|
||||
): Promise<TodoistResponse>;
|
||||
executeComment(
|
||||
ctx: Context,
|
||||
operation: CommentOperationType,
|
||||
itemIndex: number,
|
||||
): Promise<TodoistResponse>;
|
||||
executeLabel(
|
||||
ctx: Context,
|
||||
operation: LabelOperationType,
|
||||
itemIndex: number,
|
||||
): Promise<TodoistResponse>;
|
||||
executeReminder(
|
||||
ctx: Context,
|
||||
operation: ReminderOperationType,
|
||||
itemIndex: number,
|
||||
): Promise<TodoistResponse>;
|
||||
}
|
||||
|
||||
export interface TodoistProjectType {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface TodoistResponse {
|
||||
success?: boolean;
|
||||
data?: IDataObject | IDataObject[];
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,351 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import type { WorkflowTestData } from 'n8n-workflow';
|
||||
import nock from 'nock';
|
||||
|
||||
// Mock data with randomized IDs and generic names
|
||||
const projectData = {
|
||||
id: '1234567890',
|
||||
parent_id: null,
|
||||
order: 31,
|
||||
color: 'charcoal',
|
||||
name: 'Sample Project',
|
||||
comment_count: 0,
|
||||
is_shared: false,
|
||||
is_favorite: false,
|
||||
is_inbox_project: false,
|
||||
is_team_inbox: false,
|
||||
url: 'https://app.todoist.com/app/project/abc123def456',
|
||||
view_style: 'list',
|
||||
description: '',
|
||||
};
|
||||
|
||||
const sectionData = {
|
||||
id: '987654321',
|
||||
v2_id: 'sec123abc456',
|
||||
project_id: '1234567890',
|
||||
v2_project_id: 'abc123def456',
|
||||
order: 0,
|
||||
name: 'Sample Section',
|
||||
};
|
||||
|
||||
const taskData = {
|
||||
id: '5555666677',
|
||||
assigner_id: null,
|
||||
assignee_id: null,
|
||||
project_id: '1234567890',
|
||||
section_id: null,
|
||||
parent_id: null,
|
||||
order: 1,
|
||||
content: 'Sample task content',
|
||||
description: 'Sample task description',
|
||||
is_completed: false,
|
||||
labels: [],
|
||||
priority: 1,
|
||||
comment_count: 0,
|
||||
creator_id: '9876543',
|
||||
created_at: '2025-08-03T12:55:25.534632Z',
|
||||
due: {
|
||||
date: '2025-08-30',
|
||||
string: 'Next monday',
|
||||
lang: 'en',
|
||||
is_recurring: false,
|
||||
datetime: '2025-08-30T00:00:00',
|
||||
},
|
||||
url: 'https://app.todoist.com/app/task/5555666677',
|
||||
duration: null,
|
||||
deadline: null,
|
||||
};
|
||||
|
||||
const taskData2 = {
|
||||
id: '8888999900',
|
||||
assigner_id: null,
|
||||
assignee_id: null,
|
||||
project_id: '1234567890',
|
||||
section_id: null,
|
||||
parent_id: null,
|
||||
order: 3,
|
||||
content: 'Another sample task',
|
||||
description: '',
|
||||
is_completed: false,
|
||||
labels: [],
|
||||
priority: 1,
|
||||
comment_count: 0,
|
||||
creator_id: '9876543',
|
||||
created_at: '2025-08-03T12:55:31.855475Z',
|
||||
due: {
|
||||
date: '2029-03-03',
|
||||
string: '2029-03-03',
|
||||
lang: 'en',
|
||||
is_recurring: false,
|
||||
},
|
||||
url: 'https://app.todoist.com/app/task/8888999900',
|
||||
duration: {
|
||||
amount: 100,
|
||||
unit: 'minute',
|
||||
},
|
||||
deadline: {
|
||||
date: '2025-03-05',
|
||||
lang: 'en',
|
||||
},
|
||||
};
|
||||
|
||||
const labelData = {
|
||||
id: '1111222233',
|
||||
name: 'sample-label',
|
||||
color: 'red',
|
||||
order: 1,
|
||||
is_favorite: true,
|
||||
};
|
||||
|
||||
const commentData = {
|
||||
id: '4444555566',
|
||||
task_id: '5555666677',
|
||||
project_id: null,
|
||||
content: 'Sample comment',
|
||||
posted_at: '2025-08-03T12:55:30.205676Z',
|
||||
posted_by_id: '9876543',
|
||||
updated_at: '2025-08-03T12:55:30.187423Z',
|
||||
attachment: null,
|
||||
upload_id: null,
|
||||
reactions: {},
|
||||
uids_to_notify: [],
|
||||
};
|
||||
|
||||
const collaboratorData = {
|
||||
id: '9876543',
|
||||
name: 'Sample User',
|
||||
email: 'sample@example.com',
|
||||
};
|
||||
|
||||
const quickAddTaskData = {
|
||||
added_at: '2025-08-03T12:55:24.953387Z',
|
||||
added_by_uid: '9876543',
|
||||
assigned_by_uid: null,
|
||||
checked: false,
|
||||
child_order: 393,
|
||||
collapsed: false,
|
||||
completed_at: null,
|
||||
content: 'Sample quick task',
|
||||
day_order: -1,
|
||||
deadline: null,
|
||||
description: '',
|
||||
due: null,
|
||||
duration: null,
|
||||
id: '7777888899',
|
||||
is_deleted: false,
|
||||
labels: [],
|
||||
note_count: 0,
|
||||
parent_id: null,
|
||||
priority: 1,
|
||||
project_id: '1111111111',
|
||||
responsible_uid: null,
|
||||
section_id: null,
|
||||
sync_id: null,
|
||||
updated_at: '2025-08-03T12:55:24.953399Z',
|
||||
user_id: '9876543',
|
||||
v2_id: 'quick123abc',
|
||||
v2_parent_id: null,
|
||||
v2_project_id: 'inbox123abc',
|
||||
v2_section_id: null,
|
||||
};
|
||||
|
||||
const projectsListData = [
|
||||
{
|
||||
id: '1111111111',
|
||||
parent_id: null,
|
||||
order: 0,
|
||||
color: 'grey',
|
||||
name: 'Inbox',
|
||||
comment_count: 0,
|
||||
is_shared: false,
|
||||
is_favorite: false,
|
||||
is_inbox_project: true,
|
||||
is_team_inbox: false,
|
||||
url: 'https://app.todoist.com/app/project/inbox123abc',
|
||||
view_style: 'list',
|
||||
description: '',
|
||||
},
|
||||
{
|
||||
id: '2222222222',
|
||||
parent_id: null,
|
||||
order: 1,
|
||||
color: 'blue',
|
||||
name: 'Work Projects',
|
||||
comment_count: 0,
|
||||
is_shared: false,
|
||||
is_favorite: true,
|
||||
is_inbox_project: false,
|
||||
is_team_inbox: false,
|
||||
url: 'https://app.todoist.com/app/project/work123abc',
|
||||
view_style: 'board',
|
||||
description: '',
|
||||
},
|
||||
];
|
||||
|
||||
const tasksListData = [
|
||||
{
|
||||
id: '3333444455',
|
||||
assigner_id: null,
|
||||
assignee_id: null,
|
||||
project_id: '1111111111',
|
||||
section_id: '987654321',
|
||||
parent_id: null,
|
||||
order: -13,
|
||||
content: 'Sample task 1',
|
||||
description: '',
|
||||
is_completed: false,
|
||||
labels: ['work'],
|
||||
priority: 1,
|
||||
comment_count: 0,
|
||||
creator_id: '9876543',
|
||||
created_at: '2025-06-25T18:52:23.989765Z',
|
||||
due: null,
|
||||
url: 'https://app.todoist.com/app/task/3333444455',
|
||||
duration: null,
|
||||
deadline: null,
|
||||
},
|
||||
{
|
||||
id: '6666777788',
|
||||
assigner_id: null,
|
||||
assignee_id: null,
|
||||
project_id: '1111111111',
|
||||
section_id: '987654321',
|
||||
parent_id: null,
|
||||
order: -12,
|
||||
content: 'Sample task 2',
|
||||
description: '',
|
||||
is_completed: false,
|
||||
labels: ['personal'],
|
||||
priority: 1,
|
||||
comment_count: 0,
|
||||
creator_id: '9876543',
|
||||
created_at: '2025-06-22T09:58:35.471124Z',
|
||||
due: null,
|
||||
url: 'https://app.todoist.com/app/task/6666777788',
|
||||
duration: null,
|
||||
deadline: null,
|
||||
},
|
||||
];
|
||||
|
||||
const labelsListData = [
|
||||
{
|
||||
id: '1111222233',
|
||||
name: 'work',
|
||||
color: 'blue',
|
||||
order: 1,
|
||||
is_favorite: true,
|
||||
},
|
||||
{
|
||||
id: '4444555566',
|
||||
name: 'personal',
|
||||
color: 'green',
|
||||
order: 2,
|
||||
is_favorite: false,
|
||||
},
|
||||
];
|
||||
|
||||
const successResponse = { success: true };
|
||||
|
||||
describe('Execute TodoistV2 Node', () => {
|
||||
const testHarness = new NodeTestHarness();
|
||||
|
||||
beforeEach(() => {
|
||||
const todoistNock = nock('https://api.todoist.com');
|
||||
|
||||
// Project operations
|
||||
todoistNock.post('/rest/v2/projects').reply(200, projectData);
|
||||
todoistNock.get('/rest/v2/projects/1234567890').reply(200, projectData);
|
||||
todoistNock.post('/rest/v2/projects/1234567890/archive').reply(200, successResponse);
|
||||
todoistNock.post('/rest/v2/projects/1234567890/unarchive').reply(200, successResponse);
|
||||
todoistNock.post('/rest/v2/projects/1234567890').reply(200, successResponse);
|
||||
todoistNock.get('/rest/v2/projects/1234567890/collaborators').reply(200, [collaboratorData]);
|
||||
todoistNock.delete('/rest/v2/projects/1234567890').reply(200, successResponse);
|
||||
todoistNock.get('/rest/v2/projects').reply(200, projectsListData);
|
||||
|
||||
// Section operations
|
||||
todoistNock.post('/rest/v2/sections').reply(200, sectionData);
|
||||
todoistNock.get('/rest/v2/sections/987654321').reply(200, sectionData);
|
||||
todoistNock.post('/rest/v2/sections/987654321').reply(200, successResponse);
|
||||
todoistNock.delete('/rest/v2/sections/987654321').reply(200, successResponse);
|
||||
todoistNock
|
||||
.get('/rest/v2/sections')
|
||||
.query({ project_id: '1234567890' })
|
||||
.reply(200, [sectionData]);
|
||||
|
||||
// Task operations
|
||||
todoistNock.post('/rest/v2/tasks').reply(200, taskData);
|
||||
todoistNock.post('/rest/v2/tasks').reply(200, taskData2);
|
||||
todoistNock.post('/rest/v2/tasks/8888999900').reply(200, successResponse);
|
||||
todoistNock.post('/rest/v2/tasks/8888999900/close').reply(200, successResponse);
|
||||
todoistNock.post('/rest/v2/tasks/8888999900/reopen').reply(200, successResponse);
|
||||
todoistNock.delete('/rest/v2/tasks/8888999900').reply(200, successResponse);
|
||||
todoistNock.get('/rest/v2/tasks').query(true).reply(200, tasksListData);
|
||||
|
||||
// Move task uses sync API
|
||||
todoistNock.post('/sync/v9/sync').reply(200, { sync_status: { '8888999900': 'ok' } });
|
||||
|
||||
// Label operations
|
||||
todoistNock.post('/rest/v2/labels').reply(200, labelData);
|
||||
todoistNock.get('/rest/v2/labels/1111222233').reply(200, labelData);
|
||||
todoistNock.post('/rest/v2/labels/1111222233').reply(200, successResponse);
|
||||
todoistNock.delete('/rest/v2/labels/1111222233').reply(200, successResponse);
|
||||
todoistNock.get('/rest/v2/labels').reply(200, labelsListData);
|
||||
|
||||
// Comment operations
|
||||
todoistNock.post('/rest/v2/comments').reply(200, commentData);
|
||||
todoistNock.get('/rest/v2/comments/4444555566').reply(200, commentData);
|
||||
todoistNock.post('/rest/v2/comments/4444555566').reply(200, successResponse);
|
||||
todoistNock.get('/rest/v2/comments').query({ task_id: '5555666677' }).reply(200, [commentData]);
|
||||
|
||||
// Quick add operation
|
||||
todoistNock.post('/sync/v9/quick/add').reply(200, quickAddTaskData);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
});
|
||||
|
||||
const testData: WorkflowTestData = {
|
||||
description: 'Execute operations',
|
||||
input: {
|
||||
workflowData: testHarness.readWorkflowJSON('workflow.json'),
|
||||
},
|
||||
output: {
|
||||
nodeData: {
|
||||
'Create a project1': [[{ json: projectData }]],
|
||||
'Get a project': [[{ json: projectData }]],
|
||||
'Archive a project': [[{ json: successResponse }]],
|
||||
'Unarchive a project': [[{ json: successResponse }]],
|
||||
'Update a project': [[{ json: successResponse }]],
|
||||
'Get project collaborators': [[{ json: collaboratorData }]],
|
||||
'Delete a project': [[{ json: successResponse }]],
|
||||
'Get many projects': [projectsListData.map((project) => ({ json: project }))],
|
||||
'Create a section': [[{ json: sectionData }]],
|
||||
'Get a section': [[{ json: sectionData }]],
|
||||
'Update a section': [[{ json: successResponse }]],
|
||||
'Delete a section': [[{ json: successResponse }]],
|
||||
'Get many sections': [[{ json: sectionData }]],
|
||||
'Create a task': [[{ json: taskData }]],
|
||||
'Create a task1': [[{ json: taskData2 }]],
|
||||
'Update a task': [[{ json: successResponse }]],
|
||||
'Move a task': [[{ json: successResponse }]],
|
||||
'Close a task': [[{ json: successResponse }]],
|
||||
'Reopen a task': [[{ json: successResponse }]],
|
||||
'Delete a task': [[{ json: successResponse }]],
|
||||
'Get many tasks': [tasksListData.map((task) => ({ json: task }))],
|
||||
'Create a label': [[{ json: labelData }]],
|
||||
'Get a label': [[{ json: labelData }]],
|
||||
'Update a label': [[{ json: successResponse }]],
|
||||
'Delete a label': [[{ json: successResponse }]],
|
||||
'Get many labels': [labelsListData.map((label) => ({ json: label }))],
|
||||
'Create a comment': [[{ json: commentData }]],
|
||||
'Get a comment': [[{ json: commentData }]],
|
||||
'Update a comment': [[{ json: successResponse }]],
|
||||
'Get many comments': [[{ json: commentData }]],
|
||||
'Quick add a task': [[{ json: quickAddTaskData }]],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
testHarness.setupTest(testData, { credentials: { todoistApi: {} } });
|
||||
});
|
||||
@@ -0,0 +1,947 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, -112],
|
||||
"id": "ba3ea0f4-81ec-46d4-9705-7fffc01cf0df",
|
||||
"name": "When clicking ‘Execute workflow’"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "project",
|
||||
"operation": "get",
|
||||
"projectId": "={{ $json.id }}"
|
||||
},
|
||||
"type": "n8n-nodes-base.todoist",
|
||||
"typeVersion": 2.1,
|
||||
"position": [448, 80],
|
||||
"id": "d9bea9ce-cbc3-4a91-83fe-8f497aeb57d0",
|
||||
"name": "Get a project",
|
||||
"credentials": {
|
||||
"todoistApi": {
|
||||
"id": "I8WGOzhOQTmj9nfz",
|
||||
"name": "Todoist account"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "project",
|
||||
"operation": "archive",
|
||||
"projectId": "={{ $json.id }}"
|
||||
},
|
||||
"type": "n8n-nodes-base.todoist",
|
||||
"typeVersion": 2.1,
|
||||
"position": [672, 80],
|
||||
"id": "a4793b6f-1c03-4648-a750-2123fda14abd",
|
||||
"name": "Archive a project",
|
||||
"credentials": {
|
||||
"todoistApi": {
|
||||
"id": "I8WGOzhOQTmj9nfz",
|
||||
"name": "Todoist account"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "project",
|
||||
"operation": "unarchive",
|
||||
"projectId": "={{ $('Get a project').item.json.id }}"
|
||||
},
|
||||
"type": "n8n-nodes-base.todoist",
|
||||
"typeVersion": 2.1,
|
||||
"position": [896, 80],
|
||||
"id": "68a4b65b-514c-4879-807a-ff4693548f4c",
|
||||
"name": "Unarchive a project",
|
||||
"credentials": {
|
||||
"todoistApi": {
|
||||
"id": "I8WGOzhOQTmj9nfz",
|
||||
"name": "Todoist account"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "project",
|
||||
"operation": "update",
|
||||
"projectId": "={{ $('Get a project').item.json.id }}",
|
||||
"projectUpdateFields": {
|
||||
"name": "Hello world",
|
||||
"color": "red",
|
||||
"is_favorite": true,
|
||||
"view_style": "board"
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.todoist",
|
||||
"typeVersion": 2.1,
|
||||
"position": [1120, 80],
|
||||
"id": "442f5e3a-e0d3-41e5-b087-90c37efc50ff",
|
||||
"name": "Update a project",
|
||||
"credentials": {
|
||||
"todoistApi": {
|
||||
"id": "I8WGOzhOQTmj9nfz",
|
||||
"name": "Todoist account"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "project",
|
||||
"operation": "getCollaborators",
|
||||
"projectId": "={{ $('Get a project').item.json.id }}"
|
||||
},
|
||||
"type": "n8n-nodes-base.todoist",
|
||||
"typeVersion": 2.1,
|
||||
"position": [1344, 80],
|
||||
"id": "8719feca-b43b-4143-a0f1-694918e159e3",
|
||||
"name": "Get project collaborators",
|
||||
"credentials": {
|
||||
"todoistApi": {
|
||||
"id": "I8WGOzhOQTmj9nfz",
|
||||
"name": "Todoist account"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "project",
|
||||
"operation": "delete",
|
||||
"projectId": "={{ $('Get a project').item.json.id }}"
|
||||
},
|
||||
"type": "n8n-nodes-base.todoist",
|
||||
"typeVersion": 2.1,
|
||||
"position": [1568, 80],
|
||||
"id": "b8d56d72-eb9f-4e94-9405-cadb1d4e1851",
|
||||
"name": "Delete a project",
|
||||
"credentials": {
|
||||
"todoistApi": {
|
||||
"id": "I8WGOzhOQTmj9nfz",
|
||||
"name": "Todoist account"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "project",
|
||||
"operation": "getAll"
|
||||
},
|
||||
"type": "n8n-nodes-base.todoist",
|
||||
"typeVersion": 2.1,
|
||||
"position": [1792, 80],
|
||||
"id": "07a60756-c0b3-4f50-b4da-82630cbdf6f6",
|
||||
"name": "Get many projects",
|
||||
"credentials": {
|
||||
"todoistApi": {
|
||||
"id": "I8WGOzhOQTmj9nfz",
|
||||
"name": "Todoist account"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "project",
|
||||
"name": "Test",
|
||||
"projectOptions": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.todoist",
|
||||
"typeVersion": 2.1,
|
||||
"position": [224, -112],
|
||||
"id": "e5c3ba6f-1a4f-46ee-a9cb-78a106a1f57a",
|
||||
"name": "Create a project1",
|
||||
"credentials": {
|
||||
"todoistApi": {
|
||||
"id": "I8WGOzhOQTmj9nfz",
|
||||
"name": "Todoist account"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "section",
|
||||
"sectionProject": {
|
||||
"__rl": true,
|
||||
"value": "={{ $json.id }}",
|
||||
"mode": "id"
|
||||
},
|
||||
"sectionName": "Section ",
|
||||
"sectionOptions": {
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.todoist",
|
||||
"typeVersion": 2.1,
|
||||
"position": [448, -592],
|
||||
"id": "1f661708-8f3b-4cf8-b422-5d4a6ec02891",
|
||||
"name": "Create a section",
|
||||
"credentials": {
|
||||
"todoistApi": {
|
||||
"id": "I8WGOzhOQTmj9nfz",
|
||||
"name": "Todoist account"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"project": {
|
||||
"__rl": true,
|
||||
"value": "={{ $json.project_id }}",
|
||||
"mode": "id"
|
||||
},
|
||||
"content": "test content",
|
||||
"options": {
|
||||
"description": "test description",
|
||||
"dueDateTime": "2025-08-30T00:00:00",
|
||||
"dueLang": "EN",
|
||||
"dueString": "Next monday",
|
||||
"priority": 1
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.todoist",
|
||||
"typeVersion": 2.1,
|
||||
"position": [672, -592],
|
||||
"id": "692f5b29-77f2-4750-99fa-7d9a9f62a339",
|
||||
"name": "Create a task",
|
||||
"credentials": {
|
||||
"todoistApi": {
|
||||
"id": "I8WGOzhOQTmj9nfz",
|
||||
"name": "Todoist account"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "section",
|
||||
"operation": "get",
|
||||
"sectionId": "={{ $json.id }}"
|
||||
},
|
||||
"type": "n8n-nodes-base.todoist",
|
||||
"typeVersion": 2.1,
|
||||
"position": [896, -112],
|
||||
"id": "08b90997-595b-44f0-be49-7cb4d5d641f1",
|
||||
"name": "Get a section",
|
||||
"credentials": {
|
||||
"todoistApi": {
|
||||
"id": "I8WGOzhOQTmj9nfz",
|
||||
"name": "Todoist account"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "section",
|
||||
"operation": "update",
|
||||
"sectionId": "={{ $json.id }}",
|
||||
"sectionUpdateFields": {
|
||||
"name": "hello section"
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.todoist",
|
||||
"typeVersion": 2.1,
|
||||
"position": [1120, -112],
|
||||
"id": "0446c635-e9d6-491e-8bed-b0463f99192d",
|
||||
"name": "Update a section",
|
||||
"credentials": {
|
||||
"todoistApi": {
|
||||
"id": "I8WGOzhOQTmj9nfz",
|
||||
"name": "Todoist account"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "section",
|
||||
"operation": "delete",
|
||||
"sectionId": "={{ $('Get a section').item.json.id }}"
|
||||
},
|
||||
"type": "n8n-nodes-base.todoist",
|
||||
"typeVersion": 2.1,
|
||||
"position": [1344, -112],
|
||||
"id": "c396cb2f-d2a1-40d1-a478-5896ff6f5c16",
|
||||
"name": "Delete a section",
|
||||
"credentials": {
|
||||
"todoistApi": {
|
||||
"id": "I8WGOzhOQTmj9nfz",
|
||||
"name": "Todoist account"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "section",
|
||||
"operation": "getAll",
|
||||
"sectionFilters": {
|
||||
"project_id": "={{ $json.id }}"
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.todoist",
|
||||
"typeVersion": 2.1,
|
||||
"position": [624, -112],
|
||||
"id": "59ae95fd-93b4-42e4-9c11-c177b34422c4",
|
||||
"name": "Get many sections",
|
||||
"credentials": {
|
||||
"todoistApi": {
|
||||
"id": "I8WGOzhOQTmj9nfz",
|
||||
"name": "Todoist account"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "label",
|
||||
"labelName": "hot",
|
||||
"labelOptions": {
|
||||
"color": "red",
|
||||
"order": 1,
|
||||
"is_favorite": true
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.todoist",
|
||||
"typeVersion": 2.1,
|
||||
"position": [896, -688],
|
||||
"id": "028ca51f-6b0b-4200-b236-92aed48bffc3",
|
||||
"name": "Create a label",
|
||||
"credentials": {
|
||||
"todoistApi": {
|
||||
"id": "I8WGOzhOQTmj9nfz",
|
||||
"name": "Todoist account"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "label",
|
||||
"operation": "get",
|
||||
"labelId": "={{ $json.id }}"
|
||||
},
|
||||
"type": "n8n-nodes-base.todoist",
|
||||
"typeVersion": 2.1,
|
||||
"position": [1120, -688],
|
||||
"id": "0cc74ce5-6295-421c-b252-a44d354c3723",
|
||||
"name": "Get a label",
|
||||
"credentials": {
|
||||
"todoistApi": {
|
||||
"id": "I8WGOzhOQTmj9nfz",
|
||||
"name": "Todoist account"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"project": {
|
||||
"__rl": true,
|
||||
"value": "={{ $('Create a project1').item.json.id }}",
|
||||
"mode": "id"
|
||||
},
|
||||
"content": "sub test content",
|
||||
"options": {
|
||||
"order": 3,
|
||||
"dueDate": "2029-03-03",
|
||||
"assigneeId": "={{ $json.creator_id }}",
|
||||
"duration": 100,
|
||||
"durationUnit": "minute",
|
||||
"deadlineDate": "2025-03-05"
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.todoist",
|
||||
"typeVersion": 2.1,
|
||||
"position": [672, -304],
|
||||
"id": "c3971f85-6ed1-4028-becd-34a66d18846d",
|
||||
"name": "Create a task1",
|
||||
"credentials": {
|
||||
"todoistApi": {
|
||||
"id": "I8WGOzhOQTmj9nfz",
|
||||
"name": "Todoist account"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "update",
|
||||
"taskId": "={{ $json.id }}",
|
||||
"updateFields": {
|
||||
"content": "Hello world",
|
||||
"description": "my world",
|
||||
"dueDateTime": "2025-08-03T11:43:45",
|
||||
"priority": "={{ \"3\" }}",
|
||||
"duration": 100,
|
||||
"durationUnit": "day",
|
||||
"deadlineDate": "2026-03-03"
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.todoist",
|
||||
"typeVersion": 2.1,
|
||||
"position": [896, -304],
|
||||
"id": "886f2a8a-5110-408b-b932-d1ac58281000",
|
||||
"name": "Update a task",
|
||||
"credentials": {
|
||||
"todoistApi": {
|
||||
"id": "I8WGOzhOQTmj9nfz",
|
||||
"name": "Todoist account"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "move",
|
||||
"taskId": "={{ $('Create a task1').item.json.id }}",
|
||||
"project": {
|
||||
"__rl": true,
|
||||
"value": "={{ $('Create a task1').item.json.project_id }}",
|
||||
"mode": "id"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.todoist",
|
||||
"typeVersion": 2.1,
|
||||
"position": [1120, -304],
|
||||
"id": "206840f5-f7e4-48bc-b75a-62ada06d9edd",
|
||||
"name": "Move a task",
|
||||
"credentials": {
|
||||
"todoistApi": {
|
||||
"id": "I8WGOzhOQTmj9nfz",
|
||||
"name": "Todoist account"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "close",
|
||||
"taskId": "={{ $('Create a task1').item.json.id }}"
|
||||
},
|
||||
"type": "n8n-nodes-base.todoist",
|
||||
"typeVersion": 2.1,
|
||||
"position": [1344, -304],
|
||||
"id": "6e3f776a-70b5-4f8a-956b-eab674be806a",
|
||||
"name": "Close a task",
|
||||
"credentials": {
|
||||
"todoistApi": {
|
||||
"id": "I8WGOzhOQTmj9nfz",
|
||||
"name": "Todoist account"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "reopen",
|
||||
"taskId": "={{ $('Create a task1').item.json.id }}"
|
||||
},
|
||||
"type": "n8n-nodes-base.todoist",
|
||||
"typeVersion": 2.1,
|
||||
"position": [1568, -304],
|
||||
"id": "09e514ed-556e-4869-bb1d-d537122c6f16",
|
||||
"name": "Reopen a task",
|
||||
"credentials": {
|
||||
"todoistApi": {
|
||||
"id": "I8WGOzhOQTmj9nfz",
|
||||
"name": "Todoist account"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "delete",
|
||||
"taskId": "={{ $('Create a task1').item.json.id }}"
|
||||
},
|
||||
"type": "n8n-nodes-base.todoist",
|
||||
"typeVersion": 2.1,
|
||||
"position": [1792, -304],
|
||||
"id": "796f82f8-681c-4f53-aaf8-213ddce86b38",
|
||||
"name": "Delete a task",
|
||||
"credentials": {
|
||||
"todoistApi": {
|
||||
"id": "I8WGOzhOQTmj9nfz",
|
||||
"name": "Todoist account"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "label",
|
||||
"operation": "update",
|
||||
"labelId": "={{ $json.id }}",
|
||||
"labelUpdateFields": {
|
||||
"name": "test",
|
||||
"color": "orange",
|
||||
"order": 10,
|
||||
"is_favorite": false
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.todoist",
|
||||
"typeVersion": 2.1,
|
||||
"position": [1344, -688],
|
||||
"id": "5515991b-831c-4f22-b6cb-76f4f2763634",
|
||||
"name": "Update a label",
|
||||
"credentials": {
|
||||
"todoistApi": {
|
||||
"id": "I8WGOzhOQTmj9nfz",
|
||||
"name": "Todoist account"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "label",
|
||||
"operation": "delete",
|
||||
"labelId": "={{ $('Create a label').item.json.id }}"
|
||||
},
|
||||
"type": "n8n-nodes-base.todoist",
|
||||
"typeVersion": 2.1,
|
||||
"position": [1568, -688],
|
||||
"id": "be79dbb3-5b35-44d3-a99b-4954375b9dd2",
|
||||
"name": "Delete a label",
|
||||
"credentials": {
|
||||
"todoistApi": {
|
||||
"id": "I8WGOzhOQTmj9nfz",
|
||||
"name": "Todoist account"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "label",
|
||||
"operation": "getAll"
|
||||
},
|
||||
"type": "n8n-nodes-base.todoist",
|
||||
"typeVersion": 2.1,
|
||||
"position": [1792, -688],
|
||||
"id": "2e1eabe1-1322-488d-b2de-2cdd2a839d16",
|
||||
"name": "Get many labels",
|
||||
"credentials": {
|
||||
"todoistApi": {
|
||||
"id": "I8WGOzhOQTmj9nfz",
|
||||
"name": "Todoist account"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "getAll",
|
||||
"limit": 10,
|
||||
"filters": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.todoist",
|
||||
"typeVersion": 2.1,
|
||||
"position": [2016, -304],
|
||||
"id": "6694b9d6-7e2f-442d-be53-dcae97b2b59c",
|
||||
"name": "Get many tasks",
|
||||
"credentials": {
|
||||
"todoistApi": {
|
||||
"id": "I8WGOzhOQTmj9nfz",
|
||||
"name": "Todoist account"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "comment",
|
||||
"commentTaskId": "={{ $json.id }}",
|
||||
"commentContent": "my comment"
|
||||
},
|
||||
"type": "n8n-nodes-base.todoist",
|
||||
"typeVersion": 2.1,
|
||||
"position": [896, -496],
|
||||
"id": "eef98ea0-c28b-49a5-b155-4bd62bebb85c",
|
||||
"name": "Create a comment",
|
||||
"credentials": {
|
||||
"todoistApi": {
|
||||
"id": "I8WGOzhOQTmj9nfz",
|
||||
"name": "Todoist account"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "comment",
|
||||
"operation": "get",
|
||||
"commentId": "={{ $json.id }}"
|
||||
},
|
||||
"type": "n8n-nodes-base.todoist",
|
||||
"typeVersion": 2.1,
|
||||
"position": [1120, -496],
|
||||
"id": "01d4e22d-e2de-49d7-8551-cdc58016b5d5",
|
||||
"name": "Get a comment",
|
||||
"credentials": {
|
||||
"todoistApi": {
|
||||
"id": "I8WGOzhOQTmj9nfz",
|
||||
"name": "Todoist account"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "comment",
|
||||
"operation": "update",
|
||||
"commentId": "={{ $json.id }}",
|
||||
"commentUpdateFields": {
|
||||
"content": "change my comment"
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.todoist",
|
||||
"typeVersion": 2.1,
|
||||
"position": [1344, -496],
|
||||
"id": "344608b2-8f2f-49e1-8b55-35cbbc50afe5",
|
||||
"name": "Update a comment",
|
||||
"credentials": {
|
||||
"todoistApi": {
|
||||
"id": "I8WGOzhOQTmj9nfz",
|
||||
"name": "Todoist account"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "comment",
|
||||
"operation": "getAll",
|
||||
"commentFilters": {
|
||||
"task_id": "={{ $('Create a task').item.json.id }}"
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.todoist",
|
||||
"typeVersion": 2.1,
|
||||
"position": [1568, -496],
|
||||
"id": "b98749e6-a153-47d5-8f27-dbcc5d4f158c",
|
||||
"name": "Get many comments",
|
||||
"credentials": {
|
||||
"todoistApi": {
|
||||
"id": "I8WGOzhOQTmj9nfz",
|
||||
"name": "Todoist account"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "quickAdd",
|
||||
"text": "hello world!!!"
|
||||
},
|
||||
"type": "n8n-nodes-base.todoist",
|
||||
"typeVersion": 2.1,
|
||||
"position": [672, -784],
|
||||
"id": "acb416b1-2ff2-49de-9793-3e535cd61ede",
|
||||
"name": "Quick add a task",
|
||||
"credentials": {
|
||||
"todoistApi": {
|
||||
"id": "I8WGOzhOQTmj9nfz",
|
||||
"name": "Todoist account"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"When clicking ‘Execute workflow’": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Create a project1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Get a project": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Archive a project",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Archive a project": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Unarchive a project",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Unarchive a project": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Update a project",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Update a project": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get project collaborators",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Get project collaborators": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Delete a project",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Delete a project": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get many projects",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Create a project1": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get a project",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Get many sections",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Create a section",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Create a section": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Create a task",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Quick add a task",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Create a task1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Create a task": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Create a label",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Create a comment",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Get a section": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Update a section",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Update a section": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Delete a section",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Delete a section": {
|
||||
"main": [[]]
|
||||
},
|
||||
"Get many sections": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get a section",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Create a label": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get a label",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Get a label": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Update a label",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Create a task1": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Update a task",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Update a task": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Move a task",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Move a task": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Close a task",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Close a task": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Reopen a task",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Reopen a task": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Delete a task",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Delete a task": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get many tasks",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Update a label": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Delete a label",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Delete a label": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get many labels",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Create a comment": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get a comment",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Get a comment": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Update a comment",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Update a comment": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get many comments",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user