From 2abc38a1036ad108ee97a8e879ae4041963153bc Mon Sep 17 00:00:00 2001 From: Pedro Alves Date: Tue, 22 Apr 2025 17:22:43 +0200 Subject: [PATCH 1/3] refactor: move to Todoist API v1 (v2 IDs are exported instead of v1 IDs) --- package-lock.json | 247 ++++++++++++++------------- package.json | 2 +- src/services/actions.service.spec.ts | 4 +- src/services/actions.service.ts | 43 ++++- src/services/todoist.service.spec.ts | 30 ++-- src/services/todoist.service.ts | 63 ++++--- src/utils/csv-helpers.spec.ts | 8 +- test/e2e/export.e2e-spec.ts | 29 +++- test/fixtures.ts | 17 +- test/setups.ts | 2 +- 10 files changed, 275 insertions(+), 170 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1ac9cd5..ec14009 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "@doist/integrations-common": "2.0.0", - "@doist/todoist-api-typescript": "2.1.2", + "@doist/todoist-api-typescript": "4.0.1", "@doist/ui-extensions-core": "4.1.1", "@doist/ui-extensions-server": "3.3.1", "@nestjs/axios": "3.1.2", @@ -848,26 +848,19 @@ } }, "node_modules/@doist/todoist-api-typescript": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@doist/todoist-api-typescript/-/todoist-api-typescript-2.1.2.tgz", - "integrity": "sha512-6aCY8FVERMBQ+hDcADGjz76f8fZwRww3gIg05/KkcGhwqq2nfzQAPJ0d3T2F67yWTRCAmxMSupERqFJbCRXDXA==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@doist/todoist-api-typescript/-/todoist-api-typescript-4.0.1.tgz", + "integrity": "sha512-gbYomR0KSSrDKqNlXFDTrtSglsY4KbpZoMkbPZn2wcn63cGSa7XYuA5BTMW3XCypxWJrJol8l52VhcYgcZ1WhQ==", "dependencies": { - "axios": "^0.27.0", - "axios-case-converter": "^0.11.0", + "axios": "^1.0.0", + "axios-case-converter": "^1.0.0", "axios-retry": "^3.1.9", - "runtypes": "^6.5.0", "ts-custom-error": "^3.2.0", - "uuid": "^9.0.0" - } - }, - "node_modules/@doist/todoist-api-typescript/node_modules/axios": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", - "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.14.9", - "form-data": "^4.0.0" + "uuid": "^9.0.0", + "zod": "^3.24.1" + }, + "peerDependencies": { + "type-fest": "^4.12.0" } }, "node_modules/@doist/todoist-api-typescript/node_modules/uuid": { @@ -3770,9 +3763,9 @@ } }, "node_modules/axios-case-converter": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/axios-case-converter/-/axios-case-converter-0.11.1.tgz", - "integrity": "sha512-i5hrkBg7SE9jsm2Q+ClznR5DsKcYXChH6Cc3Rhx2p4gdIfJwvvO5/ATcAg/vN2UVzGE2B1eR1O4VuEGkICdJdQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/axios-case-converter/-/axios-case-converter-1.1.1.tgz", + "integrity": "sha512-v13pB7cYryh/7f4TKxN/gniD2hwqPQcjip29Hk3J9iwsnA37Rht2Hkn5VyrxynxlKdMNSIfGk6I9D6G28oTRyQ==", "dependencies": { "camel-case": "^4.1.1", "header-case": "^2.0.3", @@ -3780,13 +3773,13 @@ "tslib": "^2.3.0" }, "peerDependencies": { - "axios": ">=0.23.0 <2.0.0" + "axios": ">=1.0.0 <2.0.0" } }, "node_modules/axios-case-converter/node_modules/tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, "node_modules/axios-retry": { "version": "3.3.1", @@ -4187,9 +4180,9 @@ } }, "node_modules/camel-case/node_modules/tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, "node_modules/camelcase": { "version": "5.3.1", @@ -4231,9 +4224,9 @@ } }, "node_modules/capital-case/node_modules/tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, "node_modules/chalk": { "version": "4.1.2", @@ -4859,9 +4852,9 @@ } }, "node_modules/dot-case/node_modules/tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, "node_modules/dotenv": { "version": "16.0.3", @@ -6497,6 +6490,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globals/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/globby": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", @@ -6796,9 +6801,9 @@ } }, "node_modules/header-case/node_modules/tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, "node_modules/headers-polyfill": { "version": "3.1.2", @@ -10269,9 +10274,9 @@ } }, "node_modules/lower-case/node_modules/tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, "node_modules/lru-cache": { "version": "6.0.0", @@ -10682,9 +10687,9 @@ } }, "node_modules/no-case/node_modules/tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, "node_modules/node-abort-controller": { "version": "3.1.1", @@ -11252,9 +11257,9 @@ } }, "node_modules/pascal-case/node_modules/tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, "node_modules/path-exists": { "version": "4.0.0", @@ -11892,11 +11897,6 @@ "queue-microtask": "^1.2.2" } }, - "node_modules/runtypes": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/runtypes/-/runtypes-6.6.0.tgz", - "integrity": "sha512-ddM7sgB3fyboDlBzEYFQ04L674sKjbs4GyW2W32N/5Ae47NRd/GyMASPC2PFw8drPHYGEcZ0mZ26r5RcB8msfQ==" - }, "node_modules/rxjs": { "version": "7.8.1", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", @@ -12176,9 +12176,9 @@ } }, "node_modules/snake-case/node_modules/tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, "node_modules/source-map": { "version": "0.6.1", @@ -13041,12 +13041,12 @@ } }, "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.40.0.tgz", + "integrity": "sha512-ABHZ2/tS2JkvH1PEjxFDTUWC8dB5OsIGZP4IFLhR293GqT5Y5qB1WwL2kMPYhQW9DVgVD8Hd7I8gjwPIf5GFkw==", + "peer": true, "engines": { - "node": ">=10" + "node": ">=16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -13414,9 +13414,9 @@ } }, "node_modules/upper-case-first/node_modules/tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, "node_modules/uri-js": { "version": "4.4.1", @@ -13911,6 +13911,14 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.24.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz", + "integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } }, "dependencies": { @@ -14493,27 +14501,18 @@ "requires": {} }, "@doist/todoist-api-typescript": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@doist/todoist-api-typescript/-/todoist-api-typescript-2.1.2.tgz", - "integrity": "sha512-6aCY8FVERMBQ+hDcADGjz76f8fZwRww3gIg05/KkcGhwqq2nfzQAPJ0d3T2F67yWTRCAmxMSupERqFJbCRXDXA==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@doist/todoist-api-typescript/-/todoist-api-typescript-4.0.1.tgz", + "integrity": "sha512-gbYomR0KSSrDKqNlXFDTrtSglsY4KbpZoMkbPZn2wcn63cGSa7XYuA5BTMW3XCypxWJrJol8l52VhcYgcZ1WhQ==", "requires": { - "axios": "^0.27.0", - "axios-case-converter": "^0.11.0", + "axios": "^1.0.0", + "axios-case-converter": "^1.0.0", "axios-retry": "^3.1.9", - "runtypes": "^6.5.0", "ts-custom-error": "^3.2.0", - "uuid": "^9.0.0" + "uuid": "^9.0.0", + "zod": "^3.24.1" }, "dependencies": { - "axios": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", - "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", - "requires": { - "follow-redirects": "^1.14.9", - "form-data": "^4.0.0" - } - }, "uuid": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", @@ -16767,9 +16766,9 @@ } }, "axios-case-converter": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/axios-case-converter/-/axios-case-converter-0.11.1.tgz", - "integrity": "sha512-i5hrkBg7SE9jsm2Q+ClznR5DsKcYXChH6Cc3Rhx2p4gdIfJwvvO5/ATcAg/vN2UVzGE2B1eR1O4VuEGkICdJdQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/axios-case-converter/-/axios-case-converter-1.1.1.tgz", + "integrity": "sha512-v13pB7cYryh/7f4TKxN/gniD2hwqPQcjip29Hk3J9iwsnA37Rht2Hkn5VyrxynxlKdMNSIfGk6I9D6G28oTRyQ==", "requires": { "camel-case": "^4.1.1", "header-case": "^2.0.3", @@ -16778,9 +16777,9 @@ }, "dependencies": { "tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" } } }, @@ -17083,9 +17082,9 @@ }, "dependencies": { "tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" } } }, @@ -17112,9 +17111,9 @@ }, "dependencies": { "tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" } } }, @@ -17597,9 +17596,9 @@ }, "dependencies": { "tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" } } }, @@ -18856,6 +18855,14 @@ "dev": true, "requires": { "type-fest": "^0.20.2" + }, + "dependencies": { + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + } } }, "globby": { @@ -19077,9 +19084,9 @@ }, "dependencies": { "tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" } } }, @@ -21829,9 +21836,9 @@ }, "dependencies": { "tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" } } }, @@ -22147,9 +22154,9 @@ }, "dependencies": { "tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" } } }, @@ -22580,9 +22587,9 @@ }, "dependencies": { "tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" } } }, @@ -23040,11 +23047,6 @@ "queue-microtask": "^1.2.2" } }, - "runtypes": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/runtypes/-/runtypes-6.6.0.tgz", - "integrity": "sha512-ddM7sgB3fyboDlBzEYFQ04L674sKjbs4GyW2W32N/5Ae47NRd/GyMASPC2PFw8drPHYGEcZ0mZ26r5RcB8msfQ==" - }, "rxjs": { "version": "7.8.1", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", @@ -23265,9 +23267,9 @@ }, "dependencies": { "tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" } } }, @@ -23897,10 +23899,10 @@ "dev": true }, "type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.40.0.tgz", + "integrity": "sha512-ABHZ2/tS2JkvH1PEjxFDTUWC8dB5OsIGZP4IFLhR293GqT5Y5qB1WwL2kMPYhQW9DVgVD8Hd7I8gjwPIf5GFkw==", + "peer": true }, "type-is": { "version": "1.6.18", @@ -24096,9 +24098,9 @@ }, "dependencies": { "tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" } } }, @@ -24483,6 +24485,11 @@ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true + }, + "zod": { + "version": "3.24.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz", + "integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==" } } } diff --git a/package.json b/package.json index e11e3e2..ef74b1b 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,7 @@ "prettier": "@doist/prettier-config", "dependencies": { "@doist/integrations-common": "2.0.0", - "@doist/todoist-api-typescript": "2.1.2", + "@doist/todoist-api-typescript": "4.0.1", "@doist/ui-extensions-core": "4.1.1", "@doist/ui-extensions-server": "3.3.1", "@nestjs/axios": "3.1.2", diff --git a/src/services/actions.service.spec.ts b/src/services/actions.service.spec.ts index 86afe37..d17aaf9 100644 --- a/src/services/actions.service.spec.ts +++ b/src/services/actions.service.spec.ts @@ -121,7 +121,7 @@ describe('ActionsService', () => { setupGetAppToken('kwijibo') jest.spyOn(TodoistApi.prototype, 'getTasks').mockImplementation(() => - Promise.resolve([]), + Promise.resolve({ results: [], nextCursor: null }), ) const noTasksCard = jest.spyOn(target['adaptiveCardsService'], 'noTasksCard') @@ -199,7 +199,7 @@ describe('ActionsService', () => { const getSections = jest .spyOn(TodoistApi.prototype, 'getSections') - .mockImplementation(() => Promise.resolve([])) + .mockImplementation(() => Promise.resolve({ results: [], nextCursor: null })) await target.export({ context: { user: { id: 42 } as DoistCardContextUser, theme: 'light' }, diff --git a/src/services/actions.service.ts b/src/services/actions.service.ts index 905d877..85b4d45 100644 --- a/src/services/actions.service.ts +++ b/src/services/actions.service.ts @@ -30,6 +30,7 @@ import { GoogleSheetsService } from './google-sheets.service' import { TodoistService } from './todoist.service' import { UserDatabaseService } from './user-database.service' +import type { Section, Task, User } from '@doist/todoist-api-typescript' import type { DoistCardRequest, DoistCardResponse, @@ -206,6 +207,22 @@ export class ActionsService extends ActionsServiceBase { } } + private async fetchAllPages( + fetchFn: (cursor?: string | null) => Promise<{ results: T[]; nextCursor: string | null }>, + ): Promise { + let allResults: T[] = [] + let nextCursor: string | null | undefined = undefined + + do { + const response: { results: T[]; nextCursor: string | null } = await fetchFn(nextCursor) + + allResults = [...allResults, ...response.results] + nextCursor = response.nextCursor + } while (nextCursor !== null) + + return allResults + } + private async fetchData({ appToken, contextData, @@ -218,13 +235,18 @@ export class ActionsService extends ActionsServiceBase { const todoistClient = new TodoistApi(appToken) const [tasks, completedTasks] = await Promise.all([ - todoistClient.getTasks({ projectId: contextData.sourceId }), + this.fetchAllPages((cursor) => + todoistClient.getTasks({ + projectId: contextData.sourceId, + ...(cursor ? { cursor } : {}), + }), + ), exportOptions.includeCompleted ? this.todoistService.getCompletedTasks({ token: appToken, projectId: contextData.sourceId, }) - : [], + : Promise.resolve([]), ]) if (tasks.length === 0 && completedTasks.length === 0) { @@ -236,10 +258,21 @@ export class ActionsService extends ActionsServiceBase { } const [sections, collaborators] = await Promise.all([ - exportOptions['section'] ? todoistClient.getSections(contextData.sourceId) : [], + exportOptions['section'] + ? this.fetchAllPages
((cursor) => + todoistClient.getSections({ + projectId: contextData.sourceId, + ...(cursor ? { cursor } : {}), + }), + ) + : Promise.resolve([]), exportOptions['assignee'] - ? todoistClient.getProjectCollaborators(contextData.sourceId) - : [], + ? this.fetchAllPages((cursor) => + todoistClient.getProjectCollaborators(contextData.sourceId, { + ...(cursor ? { cursor } : {}), + }), + ) + : Promise.resolve([]), ]) return { diff --git a/src/services/todoist.service.spec.ts b/src/services/todoist.service.spec.ts index 0a2a712..7138527 100644 --- a/src/services/todoist.service.spec.ts +++ b/src/services/todoist.service.spec.ts @@ -41,11 +41,12 @@ describe('TodoistService', () => { [0, 1], [50, 1], [99, 1], - [100, 2], + [100, 1], [101, 2], [150, 2], [199, 2], - [200, 3], + [200, 2], + [201, 3], ])('when task count is %i, should call %i times', async (taskCount, expectedCalls) => { setupGetCompletedItems(Array(taskCount).fill({}) as SyncTask[]) const target = await getTarget() @@ -60,20 +61,29 @@ describe('TodoistService', () => { function setupGetCompletedItems(items: SyncTask[]) { const allTasks = chunk(items, 100) + const totalPages = allTasks.length server.use( - rest.get('https://api.todoist.com/sync/v9/items/get_completed', (req, res, ctx) => { - const offset = getOffset(req.url) - const tasks = allTasks[offset / 100] ?? [] - return res(ctx.json(tasks)) + rest.get('https://api.todoist.com/api/v9.223/archive/items', (req, res, ctx) => { + const cursor = req.url.searchParams.get('cursor') + const pageIndex = cursor ? parseInt(cursor, 10) : 0 + const tasks = allTasks[pageIndex] ?? [] + const hasMore = pageIndex < totalPages - 1 + const nextCursor = hasMore ? (pageIndex + 1).toString() : null + + return res( + ctx.json({ + total: items.length, + completed_info: [], + has_more: hasMore, + next_cursor: nextCursor, + items: tasks, + }), + ) }), ) } - function getOffset(url: URL) { - return parseInt(url.searchParams.get('offset') || '0', 10) - } - async function getTarget() { const module = await Test.createTestingModule({ imports: [HttpModule], diff --git a/src/services/todoist.service.ts b/src/services/todoist.service.ts index 95c0570..6a24cff 100644 --- a/src/services/todoist.service.ts +++ b/src/services/todoist.service.ts @@ -34,6 +34,11 @@ export type SyncTask = { section_id: string user_id: string due?: SyncDue | null + + // not exportable at the moment + // so hardcoding to null + deadline: null + duration: null } @Injectable() @@ -47,37 +52,53 @@ export class TodoistService { token: string projectId: string }): Promise { - const completedTasks = await this.getCompletedTasksInternal({ token, offset: 0, projectId }) + const completedTasks = await this.getCompletedTasksInternal({ token, projectId }) return completedTasks.map((task) => this.getTaskFromQuickAddResponse(task)) } private async getCompletedTasksInternal({ - offset, projectId, token, + cursor, }: { token: string - offset: number projectId: string + cursor?: string }): Promise { const response = await lastValueFrom( - this.httpService.get( - // At time of writing (08/02/2023), this endpoint is undocumented and its stability is not guaranteed. - `https://api.todoist.com/sync/v9/items/get_completed?project_id=${projectId}&offset=${offset}&limit=${LIMIT}`, + this.httpService.get<{ + has_more: boolean + next_cursor?: string + items: SyncTask[] + }>( + // this endpoint is not publicly documented. + // we should eventually move to one of the Todoist API v1 endpoints + // for fetching completed tasks (e.g `/tasks/completed/by_parent`). + // we're only using this endpoint because at the moment (April 2025), + // the v1 endpoints do not return data for unjoined projects. + 'https://api.todoist.com/api/v9.223/archive/items', { - headers: { - Authorization: `Bearer ${token}`, + headers: { Authorization: `Bearer ${token}` }, + params: { + limit: LIMIT, + project_id: projectId, + ...(cursor ? { cursor } : {}), }, }, ), ) - const { data: tasks } = response + const { data } = response + const tasks = data.items - if (tasks.length === LIMIT) { + if (data.has_more && data.next_cursor) { return tasks.concat( - await this.getCompletedTasksInternal({ token, offset: offset + LIMIT, projectId }), + await this.getCompletedTasksInternal({ + token, + projectId, + cursor: data.next_cursor, + }), ) } @@ -85,7 +106,7 @@ export class TodoistService { } private getTaskFromQuickAddResponse(responseData: SyncTask): Task { - const due = responseData.due + const due: Task['due'] = responseData.due ? { isRecurring: responseData.due.is_recurring, string: responseData.due.string, @@ -95,29 +116,27 @@ export class TodoistService { timezone: responseData.due.timezone, }), } - : undefined + : null - const task: Task = { + return { id: responseData.id, order: responseData.child_order, content: responseData.content, description: responseData.description, projectId: responseData.project_id, - sectionId: responseData.section_id ? responseData.section_id : undefined, + sectionId: responseData.section_id, isCompleted: responseData.checked, labels: responseData.labels, priority: responseData.priority, - commentCount: 0, // Will always be 0 for a quick add createdAt: responseData.added_at, creatorId: responseData.added_by_uid, - ...(responseData.parent_id !== null && { parentId: responseData.parent_id }), - ...(responseData.responsible_uid !== null && { - assigneeId: responseData.responsible_uid, - }), + parentId: responseData.parent_id ?? null, + assigneeId: responseData.responsible_uid ?? null, + assignerId: responseData.assigned_by_uid ?? null, completedAt: responseData.completed_at, due, + deadline: responseData.deadline, + duration: responseData.duration, } - - return task } } diff --git a/src/utils/csv-helpers.spec.ts b/src/utils/csv-helpers.spec.ts index 0ad4f03..9c7c5e5 100644 --- a/src/utils/csv-helpers.spec.ts +++ b/src/utils/csv-helpers.spec.ts @@ -129,7 +129,7 @@ describe('CSV Helpers', () => { expect(rows[1]).toEqual( toCustomCSV( - '10000001,My awesome task,1234,,false,,1,This is a description,,My awesome section,Lukas Frito (12345),2022-08-05T00:00:00.000Z,2022-08-06T00:00:00.000Z', + '10000001,My awesome task,1234,,false,2022-08-09,1,This is a description,,My awesome section,Lukas Frito (12345),2022-08-05T00:00:00.000Z,2022-08-06T00:00:00.000Z', ), ) }) @@ -170,7 +170,7 @@ describe('CSV Helpers', () => { expect(rows[1]).toEqual( toCustomCSV( - '10000001,My awesome task,1234,,false,,1,,Lukas Frito (12345),2022-08-05T00:00:00.000Z', + '10000001,My awesome task,1234,,false,2022-08-09,1,,Lukas Frito (12345),2022-08-05T00:00:00.000Z', ), ) }) @@ -213,7 +213,7 @@ describe('CSV Helpers', () => { expect(rows[1]).toEqual( toCustomCSV( - '10000001,My awesome task On two lines,1234,,false,,3,This is a description Also on two lines,,My awesome section,Lukas Frito (12345),2022-08-05T00:00:00.000Z', + '10000001,My awesome task On two lines,1234,,false,2022-08-09,3,This is a description Also on two lines,,My awesome section,Lukas Frito (12345),2022-08-05T00:00:00.000Z', ), ) }) @@ -255,7 +255,7 @@ describe('CSV Helpers', () => { const rows = result.split('\n') expect(rows[1]).toEqual( - '10000001...---...My awesome task, but with a comma...---...1234...---......---...false...---......---...1...---...This is a description Also on two lines...---......---...My awesome section...---...Lukas Frito (12345)...---...2022-08-05T00:00:00.000Z', + '10000001...---...My awesome task, but with a comma...---...1234...---......---...false...---...2022-08-09...---...1...---...This is a description Also on two lines...---......---...My awesome section...---...Lukas Frito (12345)...---...2022-08-05T00:00:00.000Z', ) }) }) diff --git a/test/e2e/export.e2e-spec.ts b/test/e2e/export.e2e-spec.ts index 1610bfe..ecf8038 100644 --- a/test/e2e/export.e2e-spec.ts +++ b/test/e2e/export.e2e-spec.ts @@ -5,8 +5,9 @@ import request from 'supertest' import { CardActions as SheetCardActions } from '../../src/constants/card-actions' import { GoogleSheetsService } from '../../src/services/google-sheets.service' +import { TodoistService } from '../../src/services/todoist.service' import { buildUser } from '../fixtures' -import { setupGetGoogleToken, setupGetUser } from '../setups' +import { setupGetGoogleToken, setupGetTasks, setupGetUser } from '../setups' import { createTestApp } from './helpers' @@ -18,6 +19,26 @@ describe('export e2e tests', () => { afterAll(() => app.close()) + beforeEach(() => { + jest.spyOn(TodoistApi.prototype, 'getTasks').mockResolvedValue({ + results: [], + nextCursor: null, + }) + jest.spyOn(TodoistApi.prototype, 'getSections').mockResolvedValue({ + results: [], + nextCursor: null, + }) + jest.spyOn(TodoistApi.prototype, 'getProjectCollaborators').mockResolvedValue({ + results: [], + nextCursor: null, + }) + jest.spyOn(TodoistApi.prototype, 'getProjectCollaborators').mockResolvedValue({ + results: [], + nextCursor: null, + }) + jest.spyOn(TodoistService.prototype, 'getCompletedTasks').mockResolvedValue([]) + }) + beforeAll(async () => { const { appModule } = await createTestApp() app = appModule @@ -27,7 +48,10 @@ describe('export e2e tests', () => { setupGetUser(buildUser()) setupGetGoogleToken('token') - jest.spyOn(TodoistApi.prototype, 'getTasks').mockImplementation(() => Promise.resolve([])) + jest.spyOn(TodoistApi.prototype, 'getTasks').mockResolvedValue({ + results: [], + nextCursor: null, + }) return request(app.getHttpServer()) .post('/process') @@ -135,6 +159,7 @@ describe('export e2e tests', () => { it('returns the error card if talking to Google fails', () => { setupGetUser(buildUser()) setupGetGoogleToken('token') + setupGetTasks() jest.spyOn(GoogleSheetsService.prototype, 'exportToSheets').mockImplementation(() => { throw new Error('Generic error talking to Google') diff --git a/test/fixtures.ts b/test/fixtures.ts index 75767b9..3829d1e 100644 --- a/test/fixtures.ts +++ b/test/fixtures.ts @@ -37,7 +37,6 @@ export const buildOptions = build('ExportOptionsToUse', { export const buildTask = build('Task', { fields: { id: sequence((num) => String(num + 10000000)), - commentCount: 0, content: perBuild(() => faker.lorem.sentence()), isCompleted: false, createdAt: perBuild(() => faker.date.recent().toDateString()), @@ -48,8 +47,13 @@ export const buildTask = build('Task', { projectId: '12345', sectionId: '12345', creatorId: '123', - assigneeId: undefined, + assigneeId: null, url: 'https://todoist.com/showTask?id=12345', + parentId: null, + deadline: null, + duration: null, + due: null, + assignerId: null, }, }) @@ -57,8 +61,15 @@ export const buildSection = build
('Section', { fields: { id: sequence((num) => String(num + 10000000)), name: perBuild(() => faker.lorem.sentence()), - order: 0, projectId: '12345', + userId: '123', + isDeleted: false, + addedAt: perBuild(() => faker.date.recent().toDateString()), + updatedAt: perBuild(() => faker.date.recent().toDateString()), + isCollapsed: false, + isArchived: false, + archivedAt: null, + sectionOrder: 0, }, }) diff --git a/test/setups.ts b/test/setups.ts index 0cf4d3e..14c6dc4 100644 --- a/test/setups.ts +++ b/test/setups.ts @@ -25,6 +25,6 @@ export function setupGetAppToken(token: string | never) { export function setupGetTasks() { jest.spyOn(TodoistApi.prototype, 'getTasks').mockImplementation(() => - Promise.resolve([buildTask()]), + Promise.resolve({ results: [buildTask()], nextCursor: null }), ) } From 557e328a43f6fdcead095b5a884252e7e8c5bb4e Mon Sep 17 00:00:00 2001 From: Pedro Alves Date: Wed, 23 Apr 2025 14:21:32 +0200 Subject: [PATCH 2/3] chore: ensure all completed tasks are fetched --- src/services/actions.service.spec.ts | 261 ++++++++++++++++++++++++++- src/services/actions.service.ts | 240 ++++++++++++++++++------ src/services/todoist.service.spec.ts | 2 +- src/services/todoist.service.ts | 197 ++++++++++++++++---- test/e2e/export.e2e-spec.ts | 5 +- 5 files changed, 606 insertions(+), 99 deletions(-) diff --git a/src/services/actions.service.spec.ts b/src/services/actions.service.spec.ts index d17aaf9..2eacb26 100644 --- a/src/services/actions.service.spec.ts +++ b/src/services/actions.service.spec.ts @@ -1,4 +1,4 @@ -import { TodoistApi } from '@doist/todoist-api-typescript' +import { Section, Task, TodoistApi } from '@doist/todoist-api-typescript' import { CoreModule, StateService } from '@doist/ui-extensions-server' import { HttpModule } from '@nestjs/axios' @@ -271,7 +271,11 @@ describe('ActionsService', () => { const getCompletedItems = jest .spyOn(TodoistService.prototype, 'getCompletedTasks') - .mockImplementation(() => Promise.resolve([])) + .mockImplementation(() => Promise.resolve({ tasks: [], completedInfo: [] })) + + jest.spyOn(TodoistService.prototype, 'getCompletedInfo').mockImplementation(() => + Promise.resolve([]), + ) await target.export({ context: { user: { id: 42 } as DoistCardContextUser, theme: 'light' }, @@ -296,6 +300,259 @@ describe('ActionsService', () => { expect(getCompletedItems).toHaveBeenCalled() }) + it('fetches completed tasks for tasks with completed subtasks', async () => { + setupGetUser(buildUser()) + setupGetGoogleToken('kwijibo') + setupGetAppToken('kwijibo') + + const parentTask = { + id: 'parent1', + projectId: '1234', + content: 'Parent Task', + isCompleted: false, + } as Task + + jest.spyOn(TodoistApi.prototype, 'getTasks').mockImplementation(() => + Promise.resolve({ results: [parentTask], nextCursor: null }), + ) + jest.spyOn(TodoistService.prototype, 'getCompletedInfo').mockImplementation(() => + Promise.resolve([{ item_id: 'parent1', completed_items: 2 }]), + ) + + const completedSubtasks = [ + { + id: 'sub1', + projectId: '1234', + content: 'Subtask 1', + description: '', + isCompleted: true, + }, + { + id: 'sub2', + projectId: '1234', + content: 'Subtask 2', + description: '', + isCompleted: true, + }, + ] as Task[] + + const getCompletedTasks = jest + .spyOn(TodoistService.prototype, 'getCompletedTasks') + .mockImplementation(({ taskId }) => { + if (taskId === 'parent1') { + return Promise.resolve({ + tasks: completedSubtasks, + completedInfo: [ + { item_id: 'sub1', completed_items: 0 }, + { item_id: 'sub2', completed_items: 0 }, + ], + }) + } + return Promise.resolve({ tasks: [], completedInfo: [] }) + }) + + jest.spyOn(target['googleSheetsService'], 'exportToSheets').mockImplementation(() => + Promise.resolve('https://docs.google.com'), + ) + + await target.export({ + context: { user: { id: 42 } as DoistCardContextUser, theme: 'light' }, + action: { + actionType: 'submit', + actionId: SheetCardActions.Export, + params: { + source: 'project', + sourceId: '1234', + url: 'https://google.com', + content: 'My Project', + contentPlain: 'My Project', + } as ContextMenuData, + inputs: { + [Inputs.IncludeCompleted]: 'true', + }, + }, + extensionType: 'context-menu', + maximumDoistCardVersion: 0.5, + }) + + expect(getCompletedTasks).toHaveBeenCalledWith( + expect.objectContaining({ taskId: 'parent1' }), + ) + }) + + it('fetches completed tasks for sections with completed tasks', async () => { + setupGetUser(buildUser()) + setupGetGoogleToken('kwijibo') + setupGetAppToken('kwijibo') + setupGetTasks() + + const sections: Section[] = [ + { + id: 'section1', + projectId: '1234', + name: 'Section 1', + userId: 'user1', + isDeleted: false, + isCollapsed: false, + isArchived: false, + archivedAt: null, + addedAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-01T00:00:00Z', + sectionOrder: 1, + }, + ] + jest.spyOn(TodoistApi.prototype, 'getSections').mockImplementation(() => + Promise.resolve({ results: sections, nextCursor: null }), + ) + + jest.spyOn(TodoistService.prototype, 'getCompletedInfo').mockImplementation(() => + Promise.resolve([{ section_id: 'section1', completed_items: 1 }]), + ) + + const completedTask = { + id: 'task1', + projectId: '1234', + content: 'Task 1', + description: '', + isCompleted: true, + } as Task + + const getCompletedTasks = jest + .spyOn(TodoistService.prototype, 'getCompletedTasks') + .mockImplementation(({ sectionId }) => { + if (sectionId === 'section1') { + return Promise.resolve({ + tasks: [completedTask], + completedInfo: [{ item_id: 'task1', completed_items: 0 }], + }) + } + return Promise.resolve({ tasks: [], completedInfo: [] }) + }) + + jest.spyOn(target['googleSheetsService'], 'exportToSheets').mockImplementation(() => + Promise.resolve('https://docs.google.com'), + ) + + await target.export({ + context: { user: { id: 42 } as DoistCardContextUser, theme: 'light' }, + action: { + actionType: 'submit', + actionId: SheetCardActions.Export, + params: { + source: 'project', + sourceId: '1234', + url: 'https://google.com', + content: 'My Project', + contentPlain: 'My Project', + } as ContextMenuData, + inputs: { + [Inputs.IncludeCompleted]: 'true', + 'Input.section': 'true', + }, + }, + extensionType: 'context-menu', + maximumDoistCardVersion: 0.5, + }) + + expect(getCompletedTasks).toHaveBeenCalledWith( + expect.objectContaining({ sectionId: 'section1' }), + ) + }) + + it('recursively fetches completed subtasks of completed tasks', async () => { + setupGetUser(buildUser()) + setupGetGoogleToken('kwijibo') + setupGetAppToken('kwijibo') + + const parentTask = { + id: 'parent1', + projectId: '1234', + content: 'Parent Task', + description: '', + isCompleted: false, + } as Task + jest.spyOn(TodoistApi.prototype, 'getTasks').mockImplementation(() => + Promise.resolve({ results: [parentTask], nextCursor: null }), + ) + + jest.spyOn(TodoistService.prototype, 'getCompletedInfo').mockImplementation(() => + Promise.resolve([{ item_id: 'parent1', completed_items: 2 }]), + ) + + const completedSubtasks = [ + { + id: 'sub1', + projectId: '1234', + content: 'Subtask 1', + description: '', + isCompleted: true, + }, + ] as Task[] + + const completedSubSubtasks = [ + { + id: 'subsub1', + projectId: '1234', + content: 'Sub-subtask 1', + description: '', + isCompleted: true, + }, + ] as Task[] + + const getCompletedTasks = jest + .spyOn(TodoistService.prototype, 'getCompletedTasks') + .mockImplementation(({ taskId }) => { + if (taskId === 'parent1') { + return Promise.resolve({ + tasks: completedSubtasks, + completedInfo: [{ item_id: 'sub1', completed_items: 1 }], + }) + } + if (taskId === 'sub1') { + return Promise.resolve({ + tasks: completedSubSubtasks, + completedInfo: [], + }) + } + return Promise.resolve({ tasks: [], completedInfo: [] }) + }) + + jest.spyOn(target['googleSheetsService'], 'exportToSheets').mockImplementation(() => + Promise.resolve('https://docs.google.com'), + ) + + await target.export({ + context: { user: { id: 42 } as DoistCardContextUser, theme: 'light' }, + action: { + actionType: 'submit', + actionId: SheetCardActions.Export, + params: { + source: 'project', + sourceId: '1234', + url: 'https://google.com', + content: 'My Project', + contentPlain: 'My Project', + } as ContextMenuData, + inputs: { + [Inputs.IncludeCompleted]: 'true', + }, + }, + extensionType: 'context-menu', + maximumDoistCardVersion: 0.5, + }) + + expect(getCompletedTasks).toHaveBeenCalledWith( + expect.objectContaining({ projectId: '1234' }), + ) + expect(getCompletedTasks).toHaveBeenCalledWith( + expect.objectContaining({ taskId: 'parent1' }), + ) + expect(getCompletedTasks).toHaveBeenCalledWith( + expect.objectContaining({ taskId: 'sub1' }), + ) + expect(getCompletedTasks).toHaveBeenCalledTimes(3) + }) + it('passes the correct data through to google sheets service', async () => { setupGetUser(buildUser()) setupGetGoogleToken('kwijibo') diff --git a/src/services/actions.service.ts b/src/services/actions.service.ts index 85b4d45..c048633 100644 --- a/src/services/actions.service.ts +++ b/src/services/actions.service.ts @@ -1,5 +1,4 @@ import { formatString } from '@doist/integrations-common' -import { TodoistApi } from '@doist/todoist-api-typescript' import { ActionsService as ActionsServiceBase, AnalyticsService, @@ -27,16 +26,16 @@ import { getExportOptions } from '../utils/input-helpers' import { AdaptiveCardService } from './adaptive-card.service' import { GoogleSheetsService } from './google-sheets.service' -import { TodoistService } from './todoist.service' +import { CompletedInfo, TodoistService } from './todoist.service' import { UserDatabaseService } from './user-database.service' -import type { Section, Task, User } from '@doist/todoist-api-typescript' +import type { Section } from '@doist/todoist-api-typescript' import type { DoistCardRequest, DoistCardResponse, TodoistContextMenuData, } from '@doist/ui-extensions-core' -import type { ExportOptionsToUse } from '../types' +import type { ExportOptionsToUse, Task } from '../types' @Injectable() export class ActionsService extends ActionsServiceBase { @@ -207,22 +206,6 @@ export class ActionsService extends ActionsServiceBase { } } - private async fetchAllPages( - fetchFn: (cursor?: string | null) => Promise<{ results: T[]; nextCursor: string | null }>, - ): Promise { - let allResults: T[] = [] - let nextCursor: string | null | undefined = undefined - - do { - const response: { results: T[]; nextCursor: string | null } = await fetchFn(nextCursor) - - allResults = [...allResults, ...response.results] - nextCursor = response.nextCursor - } while (nextCursor !== null) - - return allResults - } - private async fetchData({ appToken, contextData, @@ -232,49 +215,40 @@ export class ActionsService extends ActionsServiceBase { contextData: TodoistContextMenuData exportOptions: ExportOptionsToUse }) { - const todoistClient = new TodoistApi(appToken) - - const [tasks, completedTasks] = await Promise.all([ - this.fetchAllPages((cursor) => - todoistClient.getTasks({ - projectId: contextData.sourceId, - ...(cursor ? { cursor } : {}), - }), - ), - exportOptions.includeCompleted - ? this.todoistService.getCompletedTasks({ + const [tasks, sections, collaborators] = await Promise.all([ + this.todoistService.getProjectTasks({ + token: appToken, + projectId: contextData.sourceId, + }), + exportOptions['section'] + ? this.todoistService.getProjectSections({ token: appToken, projectId: contextData.sourceId, }) : Promise.resolve([]), - ]) - - if (tasks.length === 0 && completedTasks.length === 0) { - return { - tasks: [], - sections: [], - collaborators: [], - } - } - - const [sections, collaborators] = await Promise.all([ - exportOptions['section'] - ? this.fetchAllPages
((cursor) => - todoistClient.getSections({ - projectId: contextData.sourceId, - ...(cursor ? { cursor } : {}), - }), - ) - : Promise.resolve([]), exportOptions['assignee'] - ? this.fetchAllPages((cursor) => - todoistClient.getProjectCollaborators(contextData.sourceId, { - ...(cursor ? { cursor } : {}), - }), - ) + ? this.todoistService.getProjectCollaborators({ + token: appToken, + projectId: contextData.sourceId, + }) : Promise.resolve([]), ]) + let completedTasks: Task[] = [] + if (exportOptions.includeCompleted) { + const syncCompletedInfo = await this.todoistService.getCompletedInfo({ + token: appToken, + }) + + completedTasks = await this.fetchCompletedTasksForProject({ + appToken, + projectId: contextData.sourceId, + tasks, + sections, + syncCompletedInfo, + }) + } + return { tasks: [...tasks, ...completedTasks], sections, @@ -282,6 +256,162 @@ export class ActionsService extends ActionsServiceBase { } } + private async fetchCompletedTasksForProject(params: { + appToken: string + projectId: string + tasks: Task[] + sections: Section[] + syncCompletedInfo: CompletedInfo[] + }): Promise { + const { appToken, projectId, tasks, sections, syncCompletedInfo } = params + + const taskIdsWithCompletedSubtasks = this.findTaskIdsWithCompletedSubtasks( + syncCompletedInfo, + tasks, + ) + const sectionIdsWithCompletedTasks = this.findSectionIdsWithCompletedTasks( + syncCompletedInfo, + sections, + ) + + const [projectCompletdTasks, taskCompletedTasks, sectionCompletedTasks] = await Promise.all( + [ + this.fetchCompletedTasksForProjectId(appToken, projectId), + this.fetchCompletedTasksForTaskIds(appToken, taskIdsWithCompletedSubtasks), + this.fetchCompletedTasksForSectionIds(appToken, sectionIdsWithCompletedTasks), + ], + ) + + let allCompletedInfo = [ + ...projectCompletdTasks.completedInfo, + ...taskCompletedTasks.completedInfo, + ...sectionCompletedTasks.completedInfo, + ] + let allCompletedTasks = [ + ...projectCompletdTasks.tasks, + ...taskCompletedTasks.tasks, + ...sectionCompletedTasks.tasks, + ] + + let completedTasksIdsWithCompletedSubtasks = this.findTaskIdsWithCompletedSubtasks( + allCompletedInfo, + allCompletedTasks, + ) + + // completed tasks can have completed subtasks, so we need to loop + // until no more completed subtasks are found + while (completedTasksIdsWithCompletedSubtasks.size > 0) { + const subtaskResult = await this.fetchCompletedTasksForTaskIds( + appToken, + completedTasksIdsWithCompletedSubtasks, + ) + + allCompletedTasks = [...allCompletedTasks, ...subtaskResult.tasks] + allCompletedInfo = [...allCompletedInfo, ...subtaskResult.completedInfo] + + completedTasksIdsWithCompletedSubtasks = this.findTaskIdsWithCompletedSubtasks( + allCompletedInfo, + subtaskResult.tasks, + ) + } + + return allCompletedTasks + } + + private async fetchCompletedTasksForProjectId( + appToken: string, + projectId: string, + ): Promise<{ tasks: Task[]; completedInfo: CompletedInfo[] }> { + try { + return await this.todoistService.getCompletedTasks({ + token: appToken, + projectId, + }) + } catch (error: unknown) { + return { tasks: [], completedInfo: [] } + } + } + + private async fetchCompletedTasksForTaskIds( + appToken: string, + taskIds: Set, + ): Promise<{ tasks: Task[]; completedInfo: CompletedInfo[] }> { + if (taskIds.size === 0) { + return { tasks: [], completedInfo: [] } + } + + const fetchPromises = Array.from(taskIds).map((taskId) => + this.todoistService.getCompletedTasks({ + token: appToken, + taskId, + }), + ) + + const results = await Promise.all(fetchPromises) + + const tasks = results.flatMap((result) => result.tasks) + const completedInfo = results.flatMap((result) => result.completedInfo) + + return { tasks, completedInfo } + } + + private async fetchCompletedTasksForSectionIds( + appToken: string, + sectionIds: Set, + ): Promise<{ tasks: Task[]; completedInfo: CompletedInfo[] }> { + if (sectionIds.size === 0) { + return { tasks: [], completedInfo: [] } + } + + const fetchPromises = Array.from(sectionIds).map((sectionId) => + this.todoistService.getCompletedTasks({ + token: appToken, + sectionId, + }), + ) + + const results = await Promise.all(fetchPromises) + + const tasks = results.flatMap((result) => result.tasks) + const completedInfo = results.flatMap((result) => result.completedInfo) + + return { tasks, completedInfo } + } + + private findTaskIdsWithCompletedSubtasks( + completedInfo: CompletedInfo[], + tasks: Task[], + ): Set { + const taskIds = new Set() + const activeTaskIds = new Set(tasks.map((task) => task.id)) + + // Find tasks that have completed subtasks and are in our active tasks list + completedInfo.forEach((info) => { + if (info.item_id && activeTaskIds.has(info.item_id)) { + taskIds.add(info.item_id) + } + }) + + return taskIds + } + + private findSectionIdsWithCompletedTasks( + completedInfo: CompletedInfo[], + sections: Section[], + ): Set { + const sectionIds = new Set() + const activeSectionIds = new Set(sections.map((section) => section.id)) + + // Find sections that have completed tasks and are in our active sections list + completedInfo.forEach((info) => { + if (info.section_id && activeSectionIds.has(info.section_id)) { + sectionIds.add(info.section_id) + } + }) + + return sectionIds + } + private createSheetName(projectName: string): string { return formatString( this.translationService.getTranslation(Sheets.SHEET_TITLE), diff --git a/src/services/todoist.service.spec.ts b/src/services/todoist.service.spec.ts index 7138527..8779c83 100644 --- a/src/services/todoist.service.spec.ts +++ b/src/services/todoist.service.spec.ts @@ -33,7 +33,7 @@ describe('TodoistService', () => { projectId: '123', token: 'kwijibo', }) - expect(result).toEqual([]) + expect(result).toEqual({ tasks: [], completedInfo: [] }) expect(httpServer).toHaveBeenCalledTimes(1) }) diff --git a/src/services/todoist.service.ts b/src/services/todoist.service.ts index 6a24cff..34a96eb 100644 --- a/src/services/todoist.service.ts +++ b/src/services/todoist.service.ts @@ -1,3 +1,5 @@ +import { Section, TodoistApi, User } from '@doist/todoist-api-typescript' + import { HttpService } from '@nestjs/axios' import { Injectable } from '@nestjs/common' import { lastValueFrom } from 'rxjs' @@ -41,68 +43,183 @@ export type SyncTask = { duration: null } +export type CompletedInfo = { + item_id?: string + project_id?: string + section_id?: string + completed_items: number +} + +type CompletedTasksResponse = { + total: number + completed_info: CompletedInfo[] + has_more: boolean + next_cursor: string | null + items: SyncTask[] +} + @Injectable() export class TodoistService { constructor(private readonly httpService: HttpService) {} + async getProjectTasks(params: { token: string; projectId: string }): Promise { + const { token, projectId } = params + const todoistClient = new TodoistApi(token) + + return this.fetchAllPages((cursor) => + todoistClient.getTasks({ + projectId, + ...(cursor ? { cursor } : {}), + }), + ) + } + + async getProjectSections(params: { token: string; projectId: string }): Promise { + const { token, projectId } = params + const todoistClient = new TodoistApi(token) + + return this.fetchAllPages
((cursor) => + todoistClient.getSections({ + projectId, + ...(cursor ? { cursor } : {}), + }), + ) + } + + async getProjectCollaborators(params: { token: string; projectId: string }): Promise { + const { token, projectId } = params + const todoistClient = new TodoistApi(token) + + return this.fetchAllPages((cursor) => + todoistClient.getProjectCollaborators(projectId, { + ...(cursor ? { cursor } : {}), + }), + ) + } + + private async fetchAllPages( + fetchFn: (cursor?: string | null) => Promise<{ results: T[]; nextCursor: string | null }>, + ): Promise { + let allResults: T[] = [] + let nextCursor: string | null | undefined = undefined + + do { + const response: { results: T[]; nextCursor: string | null } = await fetchFn(nextCursor) + + allResults = [...allResults, ...response.results] + nextCursor = response.nextCursor + } while (nextCursor !== null) + + return allResults + } + async getCompletedTasks({ projectId, token, + taskId, + sectionId, }: { token: string - projectId: string - }): Promise { - const completedTasks = await this.getCompletedTasksInternal({ token, projectId }) + projectId?: string + taskId?: string + sectionId?: string + }): Promise<{ tasks: Task[]; completedInfo: CompletedInfo[] }> { + try { + const result = await this.getCompletedTasksInternal({ + token, + projectId, + taskId, + sectionId, + }) - return completedTasks.map((task) => this.getTaskFromQuickAddResponse(task)) + return { + tasks: result.items.map((task) => this.getTaskFromQuickAddResponse(task)), + completedInfo: result.completedInfo, + } + } catch (error: unknown) { + return { tasks: [], completedInfo: [] } + } } private async getCompletedTasksInternal({ projectId, token, - cursor, + taskId, + sectionId, }: { token: string - projectId: string - cursor?: string - }): Promise { - const response = await lastValueFrom( - this.httpService.get<{ - has_more: boolean - next_cursor?: string - items: SyncTask[] - }>( - // this endpoint is not publicly documented. - // we should eventually move to one of the Todoist API v1 endpoints - // for fetching completed tasks (e.g `/tasks/completed/by_parent`). - // we're only using this endpoint because at the moment (April 2025), - // the v1 endpoints do not return data for unjoined projects. - 'https://api.todoist.com/api/v9.223/archive/items', - { - headers: { Authorization: `Bearer ${token}` }, - params: { - limit: LIMIT, - project_id: projectId, - ...(cursor ? { cursor } : {}), + projectId?: string + taskId?: string + sectionId?: string + }): Promise<{ items: SyncTask[]; completedInfo: CompletedInfo[] }> { + const allItems: SyncTask[] = [] + const allCompletedInfo: CompletedInfo[] = [] + + const fetchPage = async ( + cursor?: string | null, + ): Promise<{ + results: SyncTask[] + nextCursor: string | null + completedInfo: CompletedInfo[] + }> => { + const response = await lastValueFrom( + this.httpService.get( + // this endpoint is not publicly documented. + // we should eventually move to one of the Todoist API v1 endpoints + // for fetching completed tasks (e.g `/tasks/completed/by_parent`). + // we're only using this endpoint because at the moment (April 2025), + // the v1 endpoints do not return data for unjoined projects. + 'https://api.todoist.com/api/v9.223/archive/items', + { + headers: { Authorization: `Bearer ${token}` }, + params: { + limit: LIMIT, + ...(projectId ? { project_id: projectId } : {}), + ...(taskId ? { parent_id: taskId } : {}), + ...(sectionId ? { section_id: sectionId } : {}), + ...(cursor ? { cursor } : {}), + }, }, - }, - ), - ) + ), + ) - const { data } = response - const tasks = data.items + return { + results: response.data.items, + nextCursor: response.data.has_more ? response.data.next_cursor : null, + completedInfo: response.data.completed_info, + } + } - if (data.has_more && data.next_cursor) { - return tasks.concat( - await this.getCompletedTasksInternal({ - token, - projectId, - cursor: data.next_cursor, - }), - ) + let nextCursor: string | null | undefined = undefined + + do { + const pageResult: { + results: SyncTask[] + nextCursor: string | null + completedInfo: CompletedInfo[] + } = await fetchPage(nextCursor) + + allItems.push(...pageResult.results) + allCompletedInfo.push(...pageResult.completedInfo) + nextCursor = pageResult.nextCursor + } while (nextCursor !== null) + + return { + items: allItems, + completedInfo: allCompletedInfo, } + } + + async getCompletedInfo({ token }: { token: string }): Promise { + const response = await lastValueFrom( + this.httpService.post<{ completed_info: CompletedInfo[] }>( + 'https://api.todoist.com/api/v9.223/sync', + { resource_types: ['completed_info'] }, + { headers: { Authorization: `Bearer ${token}` } }, + ), + ) - return tasks + return response.data.completed_info } private getTaskFromQuickAddResponse(responseData: SyncTask): Task { diff --git a/test/e2e/export.e2e-spec.ts b/test/e2e/export.e2e-spec.ts index ecf8038..900efe6 100644 --- a/test/e2e/export.e2e-spec.ts +++ b/test/e2e/export.e2e-spec.ts @@ -36,7 +36,10 @@ describe('export e2e tests', () => { results: [], nextCursor: null, }) - jest.spyOn(TodoistService.prototype, 'getCompletedTasks').mockResolvedValue([]) + jest.spyOn(TodoistService.prototype, 'getCompletedTasks').mockResolvedValue({ + tasks: [], + completedInfo: [], + }) }) beforeAll(async () => { From 512d4e5d01f4f2d6d4b7c35cc16d85b3e7960743 Mon Sep 17 00:00:00 2001 From: Scott Lovegrove Date: Wed, 23 Apr 2025 13:59:54 +0100 Subject: [PATCH 3/3] chore: Audit fixes --- .nsprc | 15 +++++--- package-lock.json | 88 +++++++++++++++++++---------------------------- package.json | 4 +-- 3 files changed, 48 insertions(+), 59 deletions(-) diff --git a/.nsprc b/.nsprc index 55caf20..49b53e7 100644 --- a/.nsprc +++ b/.nsprc @@ -1,9 +1,4 @@ { - "1097679": { - "active": true, - "notes": "For this to be resolved, it requires dependency updates further up the tree (like the @doist packages)", - "expiry": "2025/12/31" - }, "1099520": { "active": true, "notes": "For this to be resolved, it requires dependency updates for @nestjs/core", @@ -13,5 +8,15 @@ "active": true, "notes": "For this to be resolved, it requires dependency updates for @nestjs/core", "expiry": "2025/12/31" + }, + "1104001": { + "active": true, + "notes": "For this to be resolved, it requires dependency updates for @doist/ui-extensions-server", + "expiry": "2025/12/31" + }, + "1103903": { + "active": true, + "notes": "For this to be resolved, it requires dependency updates for @doist/ui-extensions-server", + "expiry": "2025/12/31" } } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index ec14009..eecc98a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "@doist/integrations-common": "2.0.0", + "@doist/integrations-common": "2.1.16", "@doist/todoist-api-typescript": "4.0.1", "@doist/ui-extensions-core": "4.1.1", "@doist/ui-extensions-server": "3.3.1", @@ -19,7 +19,7 @@ "@nestjs/core": "10.4.12", "@nestjs/platform-express": "10.4.12", "@nestjs/typeorm": "10.0.2", - "axios": "1.7.4", + "axios": "1.8.4", "dayjs": "1.11.7", "env-cmd": "10.1.0", "googleapis": "109.0.1", @@ -683,11 +683,12 @@ } }, "node_modules/@babel/runtime": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.9.tgz", - "integrity": "sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", + "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==", + "license": "MIT", "dependencies": { - "regenerator-runtime": "^0.13.4" + "regenerator-runtime": "^0.14.0" }, "engines": { "node": ">=6.9.0" @@ -821,21 +822,13 @@ } }, "node_modules/@doist/integrations-common": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@doist/integrations-common/-/integrations-common-2.0.0.tgz", - "integrity": "sha512-aiurW61brtZVrte78hAUtGwpLdohwLCjBVCVit+Q5BGF+RnS6vu4+80By0nXiI+Lct5cGem1MCbUlIMDGLrBhg==", + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@doist/integrations-common/-/integrations-common-2.1.16.tgz", + "integrity": "sha512-ux1/FYAvnOqXpCTacB46GqiyuEmlMBskyJQoMH9j7N/mlqhvgHZEiUMmQV3HK1s9sVGC+bpHpcdMmpvVEUEGew==", "dependencies": { - "axios": "^0.21.1", - "url-join": "^4.0.1", - "uuid": "^8.2.0" - } - }, - "node_modules/@doist/integrations-common/node_modules/axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", - "dependencies": { - "follow-redirects": "^1.14.0" + "axios": "1.8.4", + "url-join": "4.0.1", + "uuid": "8.3.2" } }, "node_modules/@doist/prettier-config": { @@ -3752,9 +3745,9 @@ } }, "node_modules/axios": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", - "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", + "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -11697,9 +11690,10 @@ "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" }, "node_modules/regenerator-runtime": { - "version": "0.13.9", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "license": "MIT" }, "node_modules/regexp.prototype.flags": { "version": "1.4.3", @@ -14369,11 +14363,11 @@ } }, "@babel/runtime": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.9.tgz", - "integrity": "sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", + "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==", "requires": { - "regenerator-runtime": "^0.13.4" + "regenerator-runtime": "^0.14.0" } }, "@babel/template": { @@ -14474,23 +14468,13 @@ } }, "@doist/integrations-common": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@doist/integrations-common/-/integrations-common-2.0.0.tgz", - "integrity": "sha512-aiurW61brtZVrte78hAUtGwpLdohwLCjBVCVit+Q5BGF+RnS6vu4+80By0nXiI+Lct5cGem1MCbUlIMDGLrBhg==", + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@doist/integrations-common/-/integrations-common-2.1.16.tgz", + "integrity": "sha512-ux1/FYAvnOqXpCTacB46GqiyuEmlMBskyJQoMH9j7N/mlqhvgHZEiUMmQV3HK1s9sVGC+bpHpcdMmpvVEUEGew==", "requires": { - "axios": "^0.21.1", - "url-join": "^4.0.1", - "uuid": "^8.2.0" - }, - "dependencies": { - "axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", - "requires": { - "follow-redirects": "^1.14.0" - } - } + "axios": "1.8.4", + "url-join": "4.0.1", + "uuid": "8.3.2" } }, "@doist/prettier-config": { @@ -16756,9 +16740,9 @@ "dev": true }, "axios": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", - "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", + "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", "requires": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -22917,9 +22901,9 @@ "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" }, "regenerator-runtime": { - "version": "0.13.9", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "regexp.prototype.flags": { "version": "1.4.3", diff --git a/package.json b/package.json index ef74b1b..fc836b4 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,7 @@ }, "prettier": "@doist/prettier-config", "dependencies": { - "@doist/integrations-common": "2.0.0", + "@doist/integrations-common": "2.1.16", "@doist/todoist-api-typescript": "4.0.1", "@doist/ui-extensions-core": "4.1.1", "@doist/ui-extensions-server": "3.3.1", @@ -98,7 +98,7 @@ "@nestjs/core": "10.4.12", "@nestjs/platform-express": "10.4.12", "@nestjs/typeorm": "10.0.2", - "axios": "1.7.4", + "axios": "1.8.4", "dayjs": "1.11.7", "env-cmd": "10.1.0", "googleapis": "109.0.1",